mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
150b8c5cdf | ||
|
|
f256e1f044 | ||
|
|
9f5e8d5547 | ||
|
|
88506d01a6 | ||
|
|
77e36e0743 | ||
|
|
ce8f438610 | ||
|
|
f3fd05d2dc | ||
|
|
1ca478cb5e | ||
|
|
efbb61b088 | ||
|
|
3984c9a938 | ||
|
|
a3c9e6727e | ||
|
|
e06a5babcf |
No files matched your search
@@ -1,6 +1,7 @@
|
||||
name: CHANGELOG.md PR check
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
|
||||
@@ -8,12 +8,29 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `--skip-build-if-not-found` flag to `eas build:dev`. ([#3495](https://github.com/expo/eas-cli/pull/3495) by [@mmichels-brex](https://github.com/mmichels-brex))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [18.3.0](https://github.com/expo/eas-cli/releases/tag/v18.3.0) - 2026-03-10
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Show user email in `eas account:view` output. ([#3485](https://github.com/expo/eas-cli/pull/3485) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [18.2.0](https://github.com/expo/eas-cli/releases/tag/v18.2.0) - 2026-03-09
|
||||
|
||||
### 🎉 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
|
||||
|
||||
- Check all certificates in provisioning profile during verification instead of only the first one. ([#3484](https://github.com/expo/eas-cli/pull/3484) by [@qwertey6](https://github.com/qwertey6))
|
||||
- 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))
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "18.2.0",
|
||||
"version": "18.3.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import { BuildTrigger, Ios, Metadata } from '@expo/eas-build-job';
|
||||
import { StepMetric } from '@expo/steps';
|
||||
|
||||
import { createTestIosJob } from './utils/job';
|
||||
import { createMockLogger } from './utils/logger';
|
||||
import { BuildContext } from '../context';
|
||||
import { CustomBuildContext } from '../customBuildContext';
|
||||
import { uploadStepMetricsToWwwAsync } from '../utils/stepMetrics';
|
||||
|
||||
jest.mock('../utils/stepMetrics');
|
||||
const mockUploadStepMetricsToWwwAsync = jest.mocked(uploadStepMetricsToWwwAsync);
|
||||
|
||||
describe(CustomBuildContext, () => {
|
||||
it('should not lose workflowInterpolationContext', () => {
|
||||
@@ -34,4 +39,122 @@ describe(CustomBuildContext, () => {
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
describe('reportStepMetric', () => {
|
||||
const expoApiV2BaseUrl = 'http://exp.test/--/api/v2/';
|
||||
const workflowJobId = 'test-workflow-job-id';
|
||||
const robotAccessToken = 'test-robot-token';
|
||||
|
||||
const sampleMetric: StepMetric = {
|
||||
metricsId: 'eas/checkout',
|
||||
result: 'success',
|
||||
durationMs: 1000,
|
||||
platform: 'linux',
|
||||
};
|
||||
|
||||
function createCustomBuildContext(
|
||||
overrides: {
|
||||
expoApiV2BaseUrl?: string | undefined;
|
||||
includeRobotAccessToken?: boolean;
|
||||
includeWorkflowJobId?: boolean;
|
||||
} = {}
|
||||
): CustomBuildContext {
|
||||
const job = createTestIosJob();
|
||||
if (overrides.includeRobotAccessToken !== false) {
|
||||
(job.secrets as any).robotAccessToken = robotAccessToken;
|
||||
}
|
||||
|
||||
const env: Record<string, string> = {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
};
|
||||
if (overrides.includeWorkflowJobId !== false) {
|
||||
env.__WORKFLOW_JOB_ID = workflowJobId;
|
||||
}
|
||||
|
||||
const ctx = new BuildContext(job, {
|
||||
env,
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
logger: createMockLogger(),
|
||||
uploadArtifact: jest.fn(),
|
||||
workingdir: '/workingdir',
|
||||
expoApiV2BaseUrl:
|
||||
'expoApiV2BaseUrl' in overrides ? overrides.expoApiV2BaseUrl : expoApiV2BaseUrl,
|
||||
});
|
||||
|
||||
return new CustomBuildContext(ctx);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUploadStepMetricsToWwwAsync.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('calls uploadStepMetricsToWwwAsync when all required fields are present', () => {
|
||||
const customCtx = createCustomBuildContext();
|
||||
customCtx.reportStepMetric(sampleMetric);
|
||||
|
||||
expect(mockUploadStepMetricsToWwwAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockUploadStepMetricsToWwwAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workflowJobId,
|
||||
robotAccessToken,
|
||||
expoApiV2BaseUrl,
|
||||
stepMetrics: [sampleMetric],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('does not call upload when workflowJobId is missing', () => {
|
||||
const customCtx = createCustomBuildContext({ includeWorkflowJobId: false });
|
||||
customCtx.reportStepMetric(sampleMetric);
|
||||
|
||||
expect(mockUploadStepMetricsToWwwAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call upload when robotAccessToken is missing', () => {
|
||||
const customCtx = createCustomBuildContext({ includeRobotAccessToken: false });
|
||||
customCtx.reportStepMetric(sampleMetric);
|
||||
|
||||
expect(mockUploadStepMetricsToWwwAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call upload when expoApiV2BaseUrl is missing', () => {
|
||||
const customCtx = createCustomBuildContext({ expoApiV2BaseUrl: undefined });
|
||||
customCtx.reportStepMetric(sampleMetric);
|
||||
|
||||
expect(mockUploadStepMetricsToWwwAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('drainPendingMetricUploads awaits all pending uploads', async () => {
|
||||
let resolveUpload!: () => void;
|
||||
const uploadPromise = new Promise<void>(resolve => {
|
||||
resolveUpload = resolve;
|
||||
});
|
||||
mockUploadStepMetricsToWwwAsync.mockReturnValue(uploadPromise);
|
||||
|
||||
const customCtx = createCustomBuildContext();
|
||||
customCtx.reportStepMetric(sampleMetric);
|
||||
|
||||
let drained = false;
|
||||
const drainPromise = customCtx.drainPendingMetricUploads().then(() => {
|
||||
drained = true;
|
||||
});
|
||||
|
||||
await new Promise(r => setImmediate(r));
|
||||
expect(drained).toBe(false);
|
||||
|
||||
resolveUpload();
|
||||
await drainPromise;
|
||||
expect(drained).toBe(true);
|
||||
});
|
||||
|
||||
it('drainPendingMetricUploads settles even if an upload rejects', async () => {
|
||||
mockUploadStepMetricsToWwwAsync.mockRejectedValue(new Error('upload failed'));
|
||||
|
||||
const customCtx = createCustomBuildContext();
|
||||
customCtx.reportStepMetric(sampleMetric);
|
||||
|
||||
await expect(customCtx.drainPendingMetricUploads()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import { BuildPhase } from '@expo/eas-build-job';
|
||||
import { BuildStepGlobalContext, StepsConfigParser } from '@expo/steps';
|
||||
|
||||
import { runGenericJobAsync } from '../generic';
|
||||
import { CustomBuildContext } from '../customBuildContext';
|
||||
import { uploadJobOutputsToWwwAsync } from '../utils/outputs';
|
||||
|
||||
jest.mock('../customBuildContext');
|
||||
jest.mock('../utils/outputs');
|
||||
jest.mock('../common/projectSources');
|
||||
jest.mock('../steps/easFunctions', () => ({ getEasFunctions: jest.fn().mockReturnValue([]) }));
|
||||
jest.mock('../steps/easFunctionGroups', () => ({
|
||||
getEasFunctionGroups: jest.fn().mockReturnValue([]),
|
||||
}));
|
||||
jest.mock('@expo/steps');
|
||||
jest.mock('fs/promises');
|
||||
|
||||
const mockUploadJobOutputsToWwwAsync = jest.mocked(uploadJobOutputsToWwwAsync);
|
||||
|
||||
describe(runGenericJobAsync, () => {
|
||||
const expoApiV2BaseUrl = 'http://exp.test/--/api/v2/';
|
||||
|
||||
let mockDrainPendingMetricUploads: jest.Mock;
|
||||
let mockCtx: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockDrainPendingMetricUploads = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
(CustomBuildContext as unknown as jest.Mock).mockImplementation(() => ({
|
||||
projectSourceDirectory: '/tmp/src',
|
||||
env: { __WORKFLOW_JOB_ID: 'test-workflow-job-id' },
|
||||
drainPendingMetricUploads: mockDrainPendingMetricUploads,
|
||||
}));
|
||||
|
||||
(BuildStepGlobalContext as unknown as jest.Mock).mockImplementation(() => ({
|
||||
env: { __WORKFLOW_JOB_ID: 'test-workflow-job-id' },
|
||||
}));
|
||||
|
||||
mockCtx = {
|
||||
expoApiV2BaseUrl,
|
||||
job: {
|
||||
secrets: { robotAccessToken: 'test-token' },
|
||||
steps: [],
|
||||
},
|
||||
logger: {
|
||||
info: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
child: jest.fn().mockReturnThis(),
|
||||
},
|
||||
runBuildPhase: jest.fn(async (_phase: BuildPhase, fn: () => Promise<any>) => fn()),
|
||||
};
|
||||
|
||||
mockUploadJobOutputsToWwwAsync.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('awaits drainPendingMetricUploads in COMPLETE_JOB phase', async () => {
|
||||
const mockWorkflow = { executeAsync: jest.fn().mockResolvedValue(undefined) };
|
||||
(StepsConfigParser as unknown as jest.Mock).mockImplementation(() => ({
|
||||
parseAsync: jest.fn().mockResolvedValue(mockWorkflow),
|
||||
}));
|
||||
|
||||
let resolveDrain!: () => void;
|
||||
mockDrainPendingMetricUploads.mockReturnValue(
|
||||
new Promise<void>(resolve => {
|
||||
resolveDrain = resolve;
|
||||
})
|
||||
);
|
||||
|
||||
let resolved = false;
|
||||
const resultPromise = runGenericJobAsync(mockCtx).then(() => {
|
||||
resolved = true;
|
||||
});
|
||||
|
||||
await new Promise(r => setImmediate(r));
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
resolveDrain();
|
||||
await resultPromise;
|
||||
expect(resolved).toBe(true);
|
||||
});
|
||||
|
||||
it('awaits drainPendingMetricUploads even when outputs upload fails', async () => {
|
||||
const mockWorkflow = { executeAsync: jest.fn().mockResolvedValue(undefined) };
|
||||
(StepsConfigParser as unknown as jest.Mock).mockImplementation(() => ({
|
||||
parseAsync: jest.fn().mockResolvedValue(mockWorkflow),
|
||||
}));
|
||||
mockUploadJobOutputsToWwwAsync.mockRejectedValue(new Error('outputs upload failed'));
|
||||
|
||||
let resolveDrain!: () => void;
|
||||
mockDrainPendingMetricUploads.mockReturnValue(
|
||||
new Promise<void>(resolve => {
|
||||
resolveDrain = resolve;
|
||||
})
|
||||
);
|
||||
|
||||
let rejected = false;
|
||||
const resultPromise = runGenericJobAsync(mockCtx).catch(err => {
|
||||
rejected = true;
|
||||
throw err;
|
||||
});
|
||||
|
||||
await new Promise(r => setImmediate(r));
|
||||
expect(rejected).toBe(false);
|
||||
|
||||
resolveDrain();
|
||||
await expect(resultPromise).rejects.toThrow('outputs upload failed');
|
||||
expect(rejected).toBe(true);
|
||||
});
|
||||
|
||||
it('throws before workflow execution when expoApiV2BaseUrl is missing', async () => {
|
||||
mockCtx.expoApiV2BaseUrl = undefined;
|
||||
|
||||
const mockWorkflow = { executeAsync: jest.fn() };
|
||||
(StepsConfigParser as unknown as jest.Mock).mockImplementation(() => ({
|
||||
parseAsync: jest.fn().mockResolvedValue(mockWorkflow),
|
||||
}));
|
||||
|
||||
await expect(runGenericJobAsync(mockCtx)).rejects.toThrow(
|
||||
'expoApiV2BaseUrl is required for generic jobs'
|
||||
);
|
||||
|
||||
expect(CustomBuildContext).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -42,7 +42,7 @@ export async function runGradleCommand(
|
||||
return line;
|
||||
}
|
||||
},
|
||||
env: { ...ctx.env, ...extraEnv, ...resolveVersionOverridesEnvs(ctx) },
|
||||
env: { ...ctx.env, ...extraEnv, ...resolveVersionOverridesEnvs(ctx), LC_ALL: 'C.UTF-8' },
|
||||
});
|
||||
if (ctx.env.EAS_BUILD_RUNNER === 'eas-build' && process.platform === 'linux') {
|
||||
adjustOOMScore(spawnPromise, logger);
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { BuildContext } from '../context';
|
||||
import { EasBuildGradle } from '../templates/EasBuildGradle';
|
||||
const APPLY_EAS_BUILD_GRADLE_LINE = 'apply from: "./eas-build.gradle"';
|
||||
|
||||
export async function configureBuildGradle(ctx: BuildContext<Android.Job>): Promise<void> {
|
||||
ctx.logger.info('Injecting signing config into build.gradle');
|
||||
if (await fs.pathExists(getEasBuildGradlePath(ctx.getReactNativeProjectDirectory()))) {
|
||||
ctx.markBuildPhaseHasWarnings();
|
||||
ctx.logger.warn('eas-build.gradle script is deprecated, please remove it from your project.');
|
||||
}
|
||||
await deleteEasBuildGradle(ctx.getReactNativeProjectDirectory());
|
||||
await createEasBuildGradle(ctx.getReactNativeProjectDirectory());
|
||||
await addApplyToBuildGradle(ctx.getReactNativeProjectDirectory());
|
||||
}
|
||||
|
||||
async function deleteEasBuildGradle(projectRoot: string): Promise<void> {
|
||||
const easBuildGradlePath = getEasBuildGradlePath(projectRoot);
|
||||
await fs.remove(easBuildGradlePath);
|
||||
}
|
||||
|
||||
function getEasBuildGradlePath(projectRoot: string): string {
|
||||
return path.join(projectRoot, 'android/app/eas-build.gradle');
|
||||
}
|
||||
|
||||
async function createEasBuildGradle(projectRoot: string): Promise<void> {
|
||||
const easBuildGradlePath = getEasBuildGradlePath(projectRoot);
|
||||
await fs.writeFile(easBuildGradlePath, EasBuildGradle);
|
||||
}
|
||||
|
||||
async function addApplyToBuildGradle(projectRoot: string): Promise<void> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectRoot);
|
||||
const buildGradleContents = await fs.readFile(path.join(buildGradlePath), 'utf8');
|
||||
|
||||
if (hasLine(buildGradleContents, APPLY_EAS_BUILD_GRADLE_LINE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
buildGradlePath,
|
||||
`${buildGradleContents.trim()}\n${APPLY_EAS_BUILD_GRADLE_LINE}\n`
|
||||
);
|
||||
}
|
||||
|
||||
function hasLine(haystack: string, needle: string): boolean {
|
||||
return (
|
||||
haystack
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
// Check for both single and double quotes
|
||||
.some(line => line === needle || line === needle.replace(/"/g, "'"))
|
||||
);
|
||||
}
|
||||
@@ -73,22 +73,20 @@ export async function resolveBuildPhaseErrorAsync(
|
||||
return error;
|
||||
}
|
||||
const xcodeBuildLogs = await maybeReadXcodeBuildLogs(phase, buildLogsDirectory);
|
||||
const userFacingError =
|
||||
error instanceof errors.UserFacingError
|
||||
const userError =
|
||||
error instanceof errors.UserError
|
||||
? error
|
||||
: (resolveError(userErrorHandlers, logLines, errorContext, xcodeBuildLogs) ??
|
||||
new errors.UnknownError(errorContext.phase));
|
||||
const buildError = resolveError(buildErrorHandlers, logLines, errorContext, xcodeBuildLogs);
|
||||
|
||||
const trackingCode =
|
||||
buildError && buildError.errorCode !== userFacingError.errorCode
|
||||
? buildError.errorCode
|
||||
: undefined;
|
||||
buildError && buildError.errorCode !== userError.errorCode ? buildError.errorCode : undefined;
|
||||
|
||||
return new errors.BuildError(userFacingError.message, {
|
||||
errorCode: userFacingError.errorCode,
|
||||
return new errors.BuildError(userError.message, {
|
||||
errorCode: userError.errorCode,
|
||||
trackingCode,
|
||||
docsUrl: userFacingError.docsUrl,
|
||||
docsUrl: userError.docsUrl,
|
||||
cause: error,
|
||||
buildPhase: phase,
|
||||
metadata: buildError?.metadata,
|
||||
|
||||
@@ -2,9 +2,9 @@ import { BuildMode, BuildPhase, Platform, Workflow, errors } from '@expo/eas-bui
|
||||
|
||||
import { ErrorHandler, XCODE_BUILD_PHASE } from './errors.types';
|
||||
|
||||
import UserFacingError = errors.UserFacingError;
|
||||
import UserError = errors.UserError;
|
||||
|
||||
export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
export const userErrorHandlers: ErrorHandler<UserError>[] = [
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.INSTALL_PODS,
|
||||
@@ -12,7 +12,7 @@ export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
// example log:
|
||||
// [!] `React` requires CocoaPods version `>= 1.10.1`, which is not satisfied by your current version, `1.10.0`.
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
new UserError(
|
||||
'EAS_BUILD_UNSUPPORTED_COCOAPODS_VERSION_ERROR',
|
||||
`Your project requires a newer version of CocoaPods. You can update it in the build profile in eas.json by either:
|
||||
- changing the current version under key "cocoapods"
|
||||
@@ -27,7 +27,7 @@ export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
// example log:
|
||||
// /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/rubygems/dependency.rb:313:in `to_specs': Could not find 'bundler' (2.2.3) required by your /Users/expo/project/build/ios/Gemfile.lock. (Gem::MissingSpecVersionError)
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
new UserError(
|
||||
'EAS_BUILD_UNSUPPORTED_BUNDLER_VERSION_ERROR',
|
||||
`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.`,
|
||||
{ docsUrl: 'https://docs.expo.dev/build-reference/eas-json/' }
|
||||
@@ -40,7 +40,7 @@ export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
// > Failed to read key keyalias from store "/build/workingdir/build/generic/keystore-5787e6af-3002-4cb7-8a57-3e73d13313c2.jks": Invalid keystore format
|
||||
regexp: /Invalid keystore format/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
new UserError(
|
||||
'EAS_BUILD_INVALID_KEYSTORE_FORMAT_ERROR',
|
||||
'The keystore used in this build is malformed or it has an unsupported type. Make sure you provided the correct file.'
|
||||
),
|
||||
@@ -52,7 +52,7 @@ export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
// > Failed to read key keyalias from store "/build/workingdir/build/generic/keystore-286069a8-4bb9-48a6-add9-acf6b58ea06d.jks": null
|
||||
regexp: /Failed to read key[^\n]+from store/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
new UserError(
|
||||
'EAS_BUILD_INVALID_KEYSTORE_ALIAS_ERROR',
|
||||
'The alias specified for this keystore does not exist. Make sure you specified the correct value.'
|
||||
),
|
||||
@@ -67,7 +67,7 @@ export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
// [11:17:29] Error: [android.dangerous]: withAndroidDangerousBaseMod: Cannot copy google-services.json from /home/expo/workingdir/build/test/test-google-services.json to /home/expo/workingdir/build/android/app/google-services.json. Please make sure the source and destination paths exist.
|
||||
regexp: /Cannot copy google-services\.json/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
new UserError(
|
||||
'EAS_BUILD_MISSING_GOOGLE_SERVICES_JSON_ERROR',
|
||||
'"google-services.json" is missing, make sure that the file exists. Remember that EAS Build only uploads the files tracked by git. Use EAS environment variables to provide EAS Build with the file.',
|
||||
{ docsUrl: 'https://docs.expo.dev/eas/environment-variables/#file-environment-variables' }
|
||||
@@ -82,7 +82,7 @@ export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
regexp:
|
||||
/File google-services\.json is missing\. The Google Services Plugin cannot function without it/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
new UserError(
|
||||
'EAS_BUILD_MISSING_GOOGLE_SERVICES_JSON_ERROR',
|
||||
'"google-services.json" is missing, make sure that the file exists. Remember that EAS Build only uploads the files tracked by git. Use EAS environment variables to provide EAS Build with the file.',
|
||||
{ docsUrl: 'https://docs.expo.dev/eas/environment-variables/#file-environment-variables' }
|
||||
@@ -95,7 +95,7 @@ export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
// [08:44:18] ENOENT: no such file or directory, copyfile '/Users/expo/workingdir/build/managed/abc' -> '/Users/expo/workingdir/build/managed/ios/testapp/GoogleService-Info.plist'
|
||||
regexp: /ENOENT: no such file or directory, copyfile .*GoogleService-Info.plist/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
new UserError(
|
||||
'EAS_BUILD_MISSING_GOOGLE_SERVICES_PLIST_ERROR',
|
||||
'"GoogleService-Info.plist" is missing, make sure that the file exists. Remember that EAS Build only uploads the files tracked by git. Use EAS environment variables to provide EAS Build with the file.',
|
||||
{ docsUrl: 'https://docs.expo.dev/eas/environment-variables/#file-environment-variables' }
|
||||
@@ -113,7 +113,7 @@ export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
regexp:
|
||||
/Specs satisfying the `(.*)` dependency were found, but they required a higher minimum deployment target/,
|
||||
createError: (_, { job }) => {
|
||||
return new UserFacingError(
|
||||
return new UserError(
|
||||
'EAS_BUILD_HIGHER_MINIMUM_DEPLOYMENT_TARGET_ERROR',
|
||||
`Some pods require a higher minimum deployment target.
|
||||
${
|
||||
@@ -141,7 +141,7 @@ ${
|
||||
// You should run `pod update Firebase/Core` to apply changes you've made.
|
||||
regexp: /CocoaPods could not find compatible versions for pod /,
|
||||
createError: () => {
|
||||
return new UserFacingError(
|
||||
return new UserError(
|
||||
'EAS_BUILD_INCOMPATIBLE_PODS_ERROR',
|
||||
`Compatible versions of some pods could not be resolved.
|
||||
You are seeing this error because either:
|
||||
@@ -161,7 +161,7 @@ You are seeing this error because either:
|
||||
/Fix the upstream dependency conflict, or retry.*\s.*this command with --force, or --legacy-peer-deps/,
|
||||
createError: (matchResult: RegExpMatchArray) => {
|
||||
if (matchResult.length >= 2) {
|
||||
return new UserFacingError(
|
||||
return new UserError(
|
||||
'EAS_BUILD_NPM_CONFLICTING_PEER_DEPENDENCIES',
|
||||
`Some of your peer dependencies are not compatible. The recommended approach is to fix your dependencies by resolving any conflicts listed by "npm install". As a temporary workaround you can:
|
||||
- Add ".npmrc" file with "legacy-peer-deps=true" and commit that to your repo.
|
||||
@@ -179,7 +179,7 @@ You are seeing this error because either:
|
||||
regexp: /Integrity check failed for "(.*)" \(computed integrity doesn't match our records, got/,
|
||||
createError: (matchResult: RegExpMatchArray) => {
|
||||
if (matchResult.length >= 2) {
|
||||
return new UserFacingError(
|
||||
return new UserError(
|
||||
'EAS_BUILD_YARN_LOCK_CHECKSUM_ERROR',
|
||||
`Checksum for package "${matchResult[1]}" does not match value in registry. To fix that:
|
||||
- run "yarn cache clean"
|
||||
@@ -203,7 +203,7 @@ You are seeing this error because either:
|
||||
/\[1\/4\] Resolving packages...\s*\[2\/4\] Fetching packages...\s*\[1\/4\] Resolving packages...\s*\[2\/4\] Fetching packages.../,
|
||||
createError: (matchResult: RegExpMatchArray) => {
|
||||
if (matchResult) {
|
||||
return new UserFacingError(
|
||||
return new UserError(
|
||||
'EAS_BUILD_YARN_MULTIPLE_INSTANCES_ERROR',
|
||||
`One of project dependencies is starting new install process while the main one is still in progress, which might result in corrupted packages. Most likely the reason for error is "prepare" script in git-referenced dependency of your project. Learn more: https://github.com/yarnpkg/yarn/issues/7212#issuecomment-493720324`
|
||||
);
|
||||
@@ -230,7 +230,7 @@ You are seeing this error because either:
|
||||
regexp: /error: Signing for "[a-zA-Z-0-9_]+" requires a development team/,
|
||||
createError: (_, { job }) =>
|
||||
'type' in job && job.type === Workflow.MANAGED
|
||||
? new UserFacingError(
|
||||
? new UserError(
|
||||
'XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR',
|
||||
`Starting from Xcode 14, resource bundles are signed by default, which requires setting the development team for each resource bundle target.
|
||||
To resolve this issue, downgrade to an older Xcode version using the "image" field in eas.json, or upgrade to SDK 46 or higher.`,
|
||||
@@ -239,7 +239,7 @@ To resolve this issue, downgrade to an older Xcode version using the "image" fie
|
||||
'https://docs.expo.dev/build-reference/infrastructure/#ios-build-server-configurations',
|
||||
}
|
||||
)
|
||||
: new UserFacingError(
|
||||
: new UserError(
|
||||
'XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR',
|
||||
`Starting from Xcode 14, resource bundles are signed by default, which requires setting the development team for each resource bundle target.
|
||||
To resolve this issue, downgrade to an older Xcode version using the "image" field in eas.json, or turn off signing resource bundles in your Podfile: https://expo.fyi/r/disable-bundle-resource-signing`,
|
||||
@@ -254,7 +254,7 @@ To resolve this issue, downgrade to an older Xcode version using the "image" fie
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
regexp: /.*/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
new UserError(
|
||||
errors.ErrorCode.UNKNOWN_GRADLE_ERROR,
|
||||
'Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.'
|
||||
),
|
||||
@@ -265,7 +265,7 @@ To resolve this issue, downgrade to an older Xcode version using the "image" fie
|
||||
mode: BuildMode.RESIGN,
|
||||
regexp: /No provisioning profile for application: '(.+)' with bundle identifier '(.+)'/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
new UserError(
|
||||
'EAS_BUILD_RESIGN_PROVISIONING_PROFILE_MISMATCH_ERROR',
|
||||
`The bundle identifier in provisioning profile used to resign the app does not match the bundle identifier of the app selected to be resigned. See logs above for more information.`
|
||||
),
|
||||
@@ -276,7 +276,7 @@ To resolve this issue, downgrade to an older Xcode version using the "image" fie
|
||||
mode: BuildMode.RESIGN,
|
||||
regexp: /.*/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
new UserError(
|
||||
errors.ErrorCode.UNKNOWN_FASTLANE_RESIGN_ERROR,
|
||||
`The "Run fastlane" step failed with an unknown error.`
|
||||
),
|
||||
@@ -297,7 +297,7 @@ To resolve this issue, downgrade to an older Xcode version using the "image" fie
|
||||
// file modified: /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework/ios-arm64/ProgrammaticAccessLibrary.framework/ProgrammaticAccessLibrary
|
||||
regexp: /error: .+/g,
|
||||
createError: matchResult =>
|
||||
new UserFacingError(
|
||||
new UserError(
|
||||
'XCODE_BUILD_ERROR',
|
||||
`The "Run fastlane" step failed because of an error in the Xcode build process. We automatically detected following errors in your Xcode build logs:\n${matchResult
|
||||
.map(match => `- ${match.replace('error: ', '')}`)
|
||||
@@ -309,7 +309,7 @@ To resolve this issue, downgrade to an older Xcode version using the "image" fie
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
regexp: /.*/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
new UserError(
|
||||
errors.ErrorCode.UNKNOWN_FASTLANE_ERROR,
|
||||
`The "Run fastlane" step failed with an unknown error. Refer to "Xcode Logs" below for additional, more detailed logs.`
|
||||
),
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
import { Android, BuildPhase, BuildPhaseResult } from '@expo/eas-build-job';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { createTestAndroidJob } from '../../__tests__/utils/job';
|
||||
import { createMockLogger } from '../../__tests__/utils/logger';
|
||||
import { BuildContext } from '../../context';
|
||||
import { restoreCredentials } from '../../android/credentials';
|
||||
import androidBuilder from '../android';
|
||||
import { runBuilderWithHooksAsync } from '../common';
|
||||
import {
|
||||
injectConfigureVersionGradleConfig,
|
||||
injectCredentialsGradleConfig,
|
||||
} from '../../steps/utils/android/gradleConfig';
|
||||
|
||||
jest.mock('../common', () => ({
|
||||
runBuilderWithHooksAsync: jest.fn(async (ctx, buildFn) => {
|
||||
await buildFn(ctx);
|
||||
return ctx.artifacts;
|
||||
}),
|
||||
}));
|
||||
jest.mock('../custom', () => ({
|
||||
runCustomBuildAsync: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../android/credentials');
|
||||
jest.mock('../../android/gradle', () => ({
|
||||
ensureLFLineEndingsInGradlewScript: jest.fn(),
|
||||
resolveGradleCommand: jest.fn(() => ':app:bundleRelease'),
|
||||
runGradleCommand: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../common/eagerBundle', () => ({
|
||||
eagerBundleAsync: jest.fn(),
|
||||
shouldUseEagerBundle: jest.fn(() => false),
|
||||
}));
|
||||
jest.mock('../../common/prebuild', () => ({
|
||||
prebuildAsync: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../common/setup', () => ({
|
||||
setupAsync: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../steps/functions/restoreBuildCache', () => ({
|
||||
cacheStatsAsync: jest.fn(),
|
||||
restoreCcacheAsync: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../steps/functions/saveBuildCache', () => ({
|
||||
saveCcacheAsync: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../steps/utils/android/gradleConfig', () => ({
|
||||
...jest.requireActual('../../steps/utils/android/gradleConfig'),
|
||||
injectConfigureVersionGradleConfig: jest.fn(),
|
||||
injectCredentialsGradleConfig: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../utils/artifacts', () => ({
|
||||
uploadApplicationArchive: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../utils/expoUpdates', () => ({
|
||||
configureExpoUpdatesIfInstalledAsync: jest.fn(),
|
||||
resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync: jest.fn(async () => null),
|
||||
}));
|
||||
jest.mock('../../utils/hooks', () => ({
|
||||
Hook: {
|
||||
POST_INSTALL: 'POST_INSTALL',
|
||||
PRE_UPLOAD_ARTIFACTS: 'PRE_UPLOAD_ARTIFACTS',
|
||||
},
|
||||
runHookIfPresent: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../utils/prepareBuildExecutable', () => ({
|
||||
prepareExecutableAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
describe(androidBuilder, () => {
|
||||
beforeEach(() => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'/workingdir/env/.gitkeep': '',
|
||||
'/workingdir/build/package.json': JSON.stringify({
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
},
|
||||
'/'
|
||||
);
|
||||
});
|
||||
|
||||
it('injects Android version config without build credentials', async () => {
|
||||
const job: Android.Job = {
|
||||
...createTestAndroidJob(),
|
||||
secrets: {
|
||||
buildCredentials: undefined,
|
||||
},
|
||||
version: {
|
||||
versionCode: '42',
|
||||
versionName: '1.2.3',
|
||||
},
|
||||
};
|
||||
const ctx = new BuildContext(job, {
|
||||
workingdir: '/workingdir',
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
logger: createMockLogger(),
|
||||
env: {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
uploadArtifact: jest.fn(),
|
||||
});
|
||||
|
||||
await androidBuilder(ctx);
|
||||
|
||||
expect(injectConfigureVersionGradleConfig).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'/workingdir/build',
|
||||
{
|
||||
versionCode: '42',
|
||||
versionName: '1.2.3',
|
||||
}
|
||||
);
|
||||
expect(injectCredentialsGradleConfig).not.toHaveBeenCalled();
|
||||
expect(restoreCredentials).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('injects Android credentials config when build credentials are present', async () => {
|
||||
const ctx = new BuildContext(createTestAndroidJob(), {
|
||||
workingdir: '/workingdir',
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
logger: createMockLogger(),
|
||||
env: {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
uploadArtifact: jest.fn(),
|
||||
});
|
||||
|
||||
await androidBuilder(ctx);
|
||||
|
||||
expect(injectCredentialsGradleConfig).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'/workingdir/build'
|
||||
);
|
||||
expect(restoreCredentials).toHaveBeenCalledWith(ctx);
|
||||
expect(injectConfigureVersionGradleConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('marks the configure Android version phase as warning for legacy eas-build.gradle', async () => {
|
||||
const reportBuildPhaseStats = jest.fn();
|
||||
const job: Android.Job = {
|
||||
...createTestAndroidJob(),
|
||||
secrets: {
|
||||
buildCredentials: undefined,
|
||||
},
|
||||
version: {
|
||||
versionCode: '42',
|
||||
versionName: '1.2.3',
|
||||
},
|
||||
};
|
||||
const ctx = new BuildContext(job, {
|
||||
workingdir: '/workingdir',
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
logger: createMockLogger(),
|
||||
env: {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
uploadArtifact: jest.fn(),
|
||||
reportBuildPhaseStats,
|
||||
});
|
||||
vol.mkdirSync('/workingdir/build/android/app', { recursive: true });
|
||||
vol.writeFileSync('/workingdir/build/android/app/eas-build.gradle', '// Legacy content');
|
||||
|
||||
await androidBuilder(ctx);
|
||||
|
||||
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
buildPhase: BuildPhase.CONFIGURE_ANDROID_VERSION,
|
||||
result: BuildPhaseResult.WARNING,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('marks the prepare credentials phase as warning for legacy eas-build.gradle', async () => {
|
||||
const reportBuildPhaseStats = jest.fn();
|
||||
const ctx = new BuildContext(createTestAndroidJob(), {
|
||||
workingdir: '/workingdir',
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
logger: createMockLogger(),
|
||||
env: {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
uploadArtifact: jest.fn(),
|
||||
reportBuildPhaseStats,
|
||||
});
|
||||
vol.mkdirSync('/workingdir/build/android/app', { recursive: true });
|
||||
vol.writeFileSync('/workingdir/build/android/app/eas-build.gradle', '// Legacy content');
|
||||
|
||||
await androidBuilder(ctx);
|
||||
|
||||
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
buildPhase: BuildPhase.PREPARE_CREDENTIALS,
|
||||
result: BuildPhaseResult.WARNING,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('injects credentials and version config when both are configured', async () => {
|
||||
const reportBuildPhaseStats = jest.fn();
|
||||
const job: Android.Job = {
|
||||
...createTestAndroidJob(),
|
||||
version: {
|
||||
versionCode: '42',
|
||||
versionName: '1.2.3',
|
||||
},
|
||||
};
|
||||
const ctx = new BuildContext(job, {
|
||||
workingdir: '/workingdir',
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
logger: createMockLogger(),
|
||||
env: {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
uploadArtifact: jest.fn(),
|
||||
reportBuildPhaseStats,
|
||||
});
|
||||
vol.mkdirSync('/workingdir/build/android/app', { recursive: true });
|
||||
vol.writeFileSync('/workingdir/build/android/app/eas-build.gradle', '// Legacy content');
|
||||
|
||||
await androidBuilder(ctx);
|
||||
|
||||
expect(injectCredentialsGradleConfig).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'/workingdir/build'
|
||||
);
|
||||
expect(restoreCredentials).toHaveBeenCalledWith(ctx);
|
||||
expect(injectConfigureVersionGradleConfig).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'/workingdir/build',
|
||||
{
|
||||
versionCode: '42',
|
||||
versionName: '1.2.3',
|
||||
}
|
||||
);
|
||||
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
buildPhase: BuildPhase.PREPARE_CREDENTIALS,
|
||||
result: BuildPhaseResult.WARNING,
|
||||
})
|
||||
);
|
||||
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
buildPhase: BuildPhase.CONFIGURE_ANDROID_VERSION,
|
||||
result: BuildPhaseResult.WARNING,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('runs the builder through hooks', async () => {
|
||||
const ctx = new BuildContext(createTestAndroidJob(), {
|
||||
workingdir: '/workingdir',
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
logger: createMockLogger(),
|
||||
env: {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
uploadArtifact: jest.fn(),
|
||||
});
|
||||
|
||||
await androidBuilder(ctx);
|
||||
|
||||
expect(runBuilderWithHooksAsync).toHaveBeenCalledWith(ctx, expect.any(Function));
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,12 @@
|
||||
import { BuildJob } from '@expo/eas-build-job';
|
||||
import { BuildWorkflow } from '@expo/steps';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { createTestIosJob } from '../../__tests__/utils/job';
|
||||
import { createMockLogger } from '../../__tests__/utils/logger';
|
||||
import { prepareProjectSourcesAsync } from '../../common/projectSources';
|
||||
import { BuildContext } from '../../context';
|
||||
import { CustomBuildContext } from '../../customBuildContext';
|
||||
import { findAndUploadXcodeBuildLogsAsync } from '../../ios/xcodeBuildLogs';
|
||||
import { runCustomBuildAsync } from '../custom';
|
||||
|
||||
@@ -74,4 +76,60 @@ describe(runCustomBuildAsync, () => {
|
||||
|
||||
expect(prepareProjectSourcesAsync).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('awaits drainPendingMetricUploads after workflow execution', async () => {
|
||||
let resolveDrain!: () => void;
|
||||
const drainSpy = jest
|
||||
.spyOn(CustomBuildContext.prototype, 'drainPendingMetricUploads')
|
||||
.mockReturnValue(
|
||||
new Promise<void>(resolve => {
|
||||
resolveDrain = resolve;
|
||||
})
|
||||
);
|
||||
|
||||
let resolved = false;
|
||||
const resultPromise = runCustomBuildAsync(ctx).then(() => {
|
||||
resolved = true;
|
||||
});
|
||||
|
||||
await new Promise(r => setImmediate(r));
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
resolveDrain();
|
||||
await resultPromise;
|
||||
expect(resolved).toBe(true);
|
||||
|
||||
drainSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('awaits drainPendingMetricUploads even when workflow throws', async () => {
|
||||
let resolveDrain!: () => void;
|
||||
const drainSpy = jest
|
||||
.spyOn(CustomBuildContext.prototype, 'drainPendingMetricUploads')
|
||||
.mockReturnValue(
|
||||
new Promise<void>(resolve => {
|
||||
resolveDrain = resolve;
|
||||
})
|
||||
);
|
||||
|
||||
const executeSpy = jest
|
||||
.spyOn(BuildWorkflow.prototype, 'executeAsync')
|
||||
.mockRejectedValue(new Error('workflow failed'));
|
||||
|
||||
let rejected = false;
|
||||
const resultPromise = runCustomBuildAsync(ctx).catch(err => {
|
||||
rejected = true;
|
||||
throw err;
|
||||
});
|
||||
|
||||
await new Promise(r => setImmediate(r));
|
||||
expect(rejected).toBe(false);
|
||||
|
||||
resolveDrain();
|
||||
await expect(resultPromise).rejects.toThrow('workflow failed');
|
||||
expect(rejected).toBe(true);
|
||||
|
||||
drainSpy.mockRestore();
|
||||
executeSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -10,13 +10,17 @@ import {
|
||||
resolveGradleCommand,
|
||||
runGradleCommand,
|
||||
} from '../android/gradle';
|
||||
import { configureBuildGradle } from '../android/gradleConfig';
|
||||
import { eagerBundleAsync, shouldUseEagerBundle } from '../common/eagerBundle';
|
||||
import { prebuildAsync } from '../common/prebuild';
|
||||
import { setupAsync } from '../common/setup';
|
||||
import { Artifacts, BuildContext, SkipNativeBuildError } from '../context';
|
||||
import { cacheStatsAsync, restoreCcacheAsync } from '../steps/functions/restoreBuildCache';
|
||||
import { saveCcacheAsync } from '../steps/functions/saveBuildCache';
|
||||
import {
|
||||
injectConfigureVersionGradleConfig,
|
||||
injectCredentialsGradleConfig,
|
||||
warnIfLegacyEasBuildGradleExists,
|
||||
} from '../steps/utils/android/gradleConfig';
|
||||
import { uploadApplicationArchive } from '../utils/artifacts';
|
||||
import {
|
||||
configureExpoUpdatesIfInstalledAsync,
|
||||
@@ -101,10 +105,22 @@ async function buildAsync(ctx: BuildContext<Android.Job>): Promise<void> {
|
||||
nullthrows(ctx.job.secrets, 'Secrets must be defined for non-custom builds').buildCredentials
|
||||
) {
|
||||
await ctx.runBuildPhase(BuildPhase.PREPARE_CREDENTIALS, async () => {
|
||||
await warnIfLegacyEasBuildGradleExists(ctx);
|
||||
await restoreCredentials(ctx);
|
||||
await configureBuildGradle(ctx);
|
||||
await injectCredentialsGradleConfig(ctx.logger, ctx.getReactNativeProjectDirectory());
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.job.version?.versionCode || ctx.job.version?.versionName) {
|
||||
await ctx.runBuildPhase(BuildPhase.CONFIGURE_ANDROID_VERSION, async () => {
|
||||
await warnIfLegacyEasBuildGradleExists(ctx);
|
||||
await injectConfigureVersionGradleConfig(ctx.logger, ctx.getReactNativeProjectDirectory(), {
|
||||
versionCode: ctx.job.version?.versionCode,
|
||||
versionName: ctx.job.version?.versionName,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => {
|
||||
await configureExpoUpdatesIfInstalledAsync(ctx, {
|
||||
resolvedRuntimeVersion: resolvedExpoUpdatesRuntimeVersion?.runtimeVersion ?? null,
|
||||
|
||||
@@ -94,6 +94,7 @@ export async function runCustomBuildAsync(ctx: BuildContext<BuildJob>): Promise<
|
||||
// do nothing, it's a non-breaking error.
|
||||
}
|
||||
}
|
||||
await customBuildCtx.drainPendingMetricUploads();
|
||||
}
|
||||
} catch (err: any) {
|
||||
err.artifacts = ctx.artifacts;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { BuildJob, BuildPhase, Ios, Job, Platform } from '@expo/eas-build-job';
|
||||
import { BuildTrigger } from '@expo/eas-build-job/dist/common';
|
||||
import { UserFacingError } from '@expo/eas-build-job/dist/errors';
|
||||
import {
|
||||
BuildJob,
|
||||
BuildPhase,
|
||||
BuildTrigger,
|
||||
Ios,
|
||||
Job,
|
||||
Platform,
|
||||
UserError,
|
||||
} from '@expo/eas-build-job';
|
||||
import spawn, { SpawnResult } from '@expo/turtle-spawn';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
@@ -281,7 +287,7 @@ async function validateAppConfigAsync(
|
||||
extraMessage =
|
||||
'If you are using environment variables to switch between projects in app.config.js/app.config.ts, make sure those variables are also set inside EAS Build. You can do that using "env" field in eas.json or EAS environment variables. ';
|
||||
}
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'EAS_BUILD_PROJECT_ID_MISMATCH',
|
||||
`The value of the "extra.eas.projectId" field (${appConfig.extra.eas.projectId}) in the app config does not match the current project id (${ctx.env.EAS_BUILD_PROJECT_ID}). ${extraMessage}Learn more: https://expo.fyi/eas-config-mismatch.`
|
||||
);
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface BuildContextOptions {
|
||||
reportBuildPhaseStats?: (stats: BuildPhaseStats) => void;
|
||||
skipNativeBuild?: boolean;
|
||||
metadata?: Metadata;
|
||||
expoApiV2BaseUrl?: string;
|
||||
}
|
||||
|
||||
export class SkipNativeBuildError extends Error {}
|
||||
@@ -82,6 +83,7 @@ export class BuildContext<TJob extends Job = Job> {
|
||||
options?: { tags?: Record<string, string>; extras?: Record<string, string> }
|
||||
) => void;
|
||||
public readonly skipNativeBuild?: boolean;
|
||||
public readonly expoApiV2BaseUrl?: string;
|
||||
public artifacts: Artifacts = {};
|
||||
|
||||
private readonly _isLocal: boolean;
|
||||
@@ -110,6 +112,7 @@ export class BuildContext<TJob extends Job = Job> {
|
||||
this._job = job;
|
||||
this._metadata = options.metadata;
|
||||
this.skipNativeBuild = options.skipNativeBuild;
|
||||
this.expoApiV2BaseUrl = options.expoApiV2BaseUrl;
|
||||
this.reportBuildPhaseStats = options.reportBuildPhaseStats;
|
||||
|
||||
const environmentSecrets = this.getEnvironmentSecrets(job);
|
||||
|
||||
@@ -9,12 +9,13 @@ import {
|
||||
StaticJobInterpolationContext,
|
||||
} from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { BuildRuntimePlatform, ExternalBuildContextProvider } from '@expo/steps';
|
||||
import { BuildRuntimePlatform, ExternalBuildContextProvider, StepMetric } from '@expo/steps';
|
||||
import { Client } from '@urql/core';
|
||||
import assert from 'assert';
|
||||
import path from 'path';
|
||||
|
||||
import { ArtifactToUpload, BuildContext } from './context';
|
||||
import { uploadStepMetricsToWwwAsync } from './utils/stepMetrics';
|
||||
|
||||
const platformToBuildRuntimePlatform: Record<Platform, BuildRuntimePlatform> = {
|
||||
[Platform.ANDROID]: BuildRuntimePlatform.LINUX,
|
||||
@@ -60,6 +61,8 @@ export class CustomBuildContext<TJob extends Job = Job> implements ExternalBuild
|
||||
public metadata?: Metadata;
|
||||
|
||||
private _env: Env;
|
||||
private readonly expoApiV2BaseUrl?: string;
|
||||
private readonly pendingMetricUploads: Promise<void>[] = [];
|
||||
|
||||
constructor(buildCtx: BuildContext<TJob>) {
|
||||
this._env = buildCtx.env;
|
||||
@@ -75,6 +78,7 @@ export class CustomBuildContext<TJob extends Job = Job> implements ExternalBuild
|
||||
this.runtimeApi = {
|
||||
uploadArtifact: (...args) => buildCtx['uploadArtifact'](...args),
|
||||
};
|
||||
this.expoApiV2BaseUrl = buildCtx.expoApiV2BaseUrl;
|
||||
this.startTime = new Date();
|
||||
}
|
||||
|
||||
@@ -142,4 +146,26 @@ export class CustomBuildContext<TJob extends Job = Job> implements ExternalBuild
|
||||
};
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public reportStepMetric(metric: StepMetric): void {
|
||||
const workflowJobId = this._env.__WORKFLOW_JOB_ID;
|
||||
const robotAccessToken = this.job.secrets?.robotAccessToken;
|
||||
|
||||
if (!workflowJobId || !robotAccessToken || !this.expoApiV2BaseUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = uploadStepMetricsToWwwAsync({
|
||||
workflowJobId,
|
||||
robotAccessToken,
|
||||
expoApiV2BaseUrl: this.expoApiV2BaseUrl,
|
||||
stepMetrics: [metric],
|
||||
logger: this.logger,
|
||||
});
|
||||
this.pendingMetricUploads.push(p);
|
||||
}
|
||||
|
||||
public async drainPendingMetricUploads(): Promise<void> {
|
||||
await Promise.allSettled(this.pendingMetricUploads);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { BuildPhase, Generic } from '@expo/eas-build-job';
|
||||
import { Result, asyncResult } from '@expo/results';
|
||||
import { BuildStepGlobalContext, BuildWorkflow, StepsConfigParser, errors } from '@expo/steps';
|
||||
import fs from 'fs/promises';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { prepareProjectSourcesAsync } from './common/projectSources';
|
||||
import { BuildContext } from './context';
|
||||
@@ -10,12 +11,17 @@ import { getEasFunctionGroups } from './steps/easFunctionGroups';
|
||||
import { getEasFunctions } from './steps/easFunctions';
|
||||
import { uploadJobOutputsToWwwAsync } from './utils/outputs';
|
||||
import { retryAsync } from './utils/retry';
|
||||
import { uploadStepMetricsToWwwAsync } from './utils/stepMetrics';
|
||||
|
||||
export async function runGenericJobAsync(
|
||||
ctx: BuildContext<Generic.Job>,
|
||||
{ expoApiV2BaseUrl }: { expoApiV2BaseUrl: string }
|
||||
ctx: BuildContext<Generic.Job>
|
||||
): Promise<{ runResult: Result<void>; buildWorkflow: BuildWorkflow }> {
|
||||
// expoApiV2BaseUrl is empty when running a local build, but generic jobs
|
||||
// are never executed locally, so it is always present here.
|
||||
const expoApiV2BaseUrl = nullthrows(
|
||||
ctx.expoApiV2BaseUrl,
|
||||
'expoApiV2BaseUrl is required for generic jobs'
|
||||
);
|
||||
|
||||
const customBuildCtx = new CustomBuildContext(ctx);
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.PREPARE_PROJECT, async () => {
|
||||
@@ -60,22 +66,15 @@ export async function runGenericJobAsync(
|
||||
const runResult = await asyncResult(workflow.executeAsync());
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.COMPLETE_JOB, async () => {
|
||||
await uploadJobOutputsToWwwAsync(globalContext, {
|
||||
logger: ctx.logger,
|
||||
expoApiV2BaseUrl,
|
||||
});
|
||||
|
||||
const workflowJobId = globalContext.env.__WORKFLOW_JOB_ID;
|
||||
const robotAccessToken = ctx.job.secrets?.robotAccessToken;
|
||||
|
||||
if (workflowJobId && robotAccessToken) {
|
||||
await uploadStepMetricsToWwwAsync({
|
||||
workflowJobId,
|
||||
robotAccessToken,
|
||||
expoApiV2BaseUrl,
|
||||
stepMetrics: globalContext.stepMetrics,
|
||||
const results = await Promise.allSettled([
|
||||
uploadJobOutputsToWwwAsync(globalContext, {
|
||||
logger: ctx.logger,
|
||||
});
|
||||
expoApiV2BaseUrl,
|
||||
}),
|
||||
customBuildCtx.drainPendingMetricUploads(),
|
||||
]);
|
||||
if (results[0].status === 'rejected') {
|
||||
throw results[0].reason;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Ios } from '@expo/eas-build-job';
|
||||
import { createLogger } from '@expo/logger';
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { provisioningProfile } from './fixtures';
|
||||
import { BuildContext } from '../../../context';
|
||||
@@ -73,5 +74,45 @@ describe('ProvisioningProfile class', () => {
|
||||
await pp.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it('should match a certificate that is not at index 0 in DeveloperCertificates', async () => {
|
||||
const pp = new ProvisioningProfile(
|
||||
ctx,
|
||||
Buffer.from(provisioningProfile.dataBase64, 'base64'),
|
||||
keychain.data.path,
|
||||
'testapp',
|
||||
'Abc 123'
|
||||
);
|
||||
|
||||
try {
|
||||
await pp.init();
|
||||
|
||||
// Prepend an unrelated certificate so the real one moves to index 1
|
||||
const dummyCert = Buffer.from('dummy-certificate-data');
|
||||
(pp as any).developerCertificates = [dummyCert, ...(pp as any).developerCertificates];
|
||||
|
||||
// The original cert fingerprint should still match
|
||||
expect(() => {
|
||||
pp.verifyCertificate(provisioningProfile.certFingerprint);
|
||||
}).not.toThrow();
|
||||
|
||||
// A non-matching fingerprint should still fail
|
||||
expect(() => {
|
||||
pp.verifyCertificate('2137');
|
||||
}).toThrowError(/don't match/);
|
||||
|
||||
// The dummy cert's fingerprint should also match
|
||||
const dummyFingerprint = crypto
|
||||
.createHash('sha1')
|
||||
.update(new Uint8Array(dummyCert))
|
||||
.digest('hex')
|
||||
.toUpperCase();
|
||||
expect(() => {
|
||||
pp.verifyCertificate(dummyFingerprint);
|
||||
}).not.toThrow();
|
||||
} finally {
|
||||
await pp.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -43,6 +43,7 @@ export default class ProvisioningProfile<TJob extends Ios.Job> {
|
||||
|
||||
private readonly profilePath: string;
|
||||
private profileData?: ProvisioningProfileData;
|
||||
private developerCertificates: Buffer[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly ctx: BuildContext<TJob>,
|
||||
@@ -77,11 +78,12 @@ export default class ProvisioningProfile<TJob extends Ios.Job> {
|
||||
}
|
||||
|
||||
public verifyCertificate(fingerprint: string): void {
|
||||
const devCertFingerprint = this.genDerCertFingerprint();
|
||||
if (devCertFingerprint !== fingerprint) {
|
||||
const devCertFingerprints = this.getAllDerCertFingerprints();
|
||||
if (!devCertFingerprints.includes(fingerprint)) {
|
||||
throw new errors.CredentialsDistCertMismatchError(
|
||||
`Provisioning profile and distribution certificate don't match.
|
||||
Profile's certificate fingerprint = ${devCertFingerprint}, distribution certificate fingerprint = ${fingerprint}`
|
||||
`Provisioning profile and distribution certificate don't match.\n` +
|
||||
`Profile's certificate fingerprints = [${devCertFingerprints.join(', ')}], ` +
|
||||
`distribution certificate fingerprint = ${fingerprint}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -114,6 +116,10 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
|
||||
] as string;
|
||||
const bundleIdentifier = applicationIdentifier.replace(/^.+?\./, '');
|
||||
|
||||
this.developerCertificates = (plistData.DeveloperCertificates as string[]).map((cert: string) =>
|
||||
Buffer.from(cert, 'base64')
|
||||
);
|
||||
|
||||
this.profileData = {
|
||||
path: this.profilePath,
|
||||
target: this.target,
|
||||
@@ -121,7 +127,7 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
|
||||
teamId: (plistData.TeamIdentifier as string[])[0],
|
||||
uuid: plistData.UUID as string,
|
||||
name: plistData.Name as string,
|
||||
developerCertificate: Buffer.from((plistData.DeveloperCertificates as string[])[0], 'base64'),
|
||||
developerCertificate: this.developerCertificates[0],
|
||||
certificateCommonName: this.certificateCommonName,
|
||||
distributionType: this.resolveDistributionType(plistData),
|
||||
};
|
||||
@@ -137,11 +143,9 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
|
||||
}
|
||||
}
|
||||
|
||||
private genDerCertFingerprint(): string {
|
||||
return crypto
|
||||
.createHash('sha1')
|
||||
.update(new Uint8Array(this.data.developerCertificate))
|
||||
.digest('hex')
|
||||
.toUpperCase();
|
||||
private getAllDerCertFingerprints(): string[] {
|
||||
return this.developerCertificates.map(cert =>
|
||||
crypto.createHash('sha1').update(new Uint8Array(cert)).digest('hex').toUpperCase()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserFacingError } from '@expo/eas-build-job/dist/errors';
|
||||
import { UserError } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { asyncResult } from '@expo/results';
|
||||
import {
|
||||
@@ -56,7 +56,7 @@ export function createDownloadArtifactFunction(): BuildFunction {
|
||||
const interpolationContext = stepsCtx.global.getInterpolationContext();
|
||||
|
||||
if (!('workflow' in interpolationContext)) {
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'EAS_DOWNLOAD_ARTIFACT_NO_WORKFLOW',
|
||||
'No workflow found in the interpolation context.'
|
||||
);
|
||||
@@ -64,7 +64,7 @@ export function createDownloadArtifactFunction(): BuildFunction {
|
||||
|
||||
const robotAccessToken = stepsCtx.global.staticContext.job.secrets?.robotAccessToken;
|
||||
if (!robotAccessToken) {
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'EAS_DOWNLOAD_ARTIFACT_NO_ROBOT_ACCESS_TOKEN',
|
||||
'No robot access token found in the job secrets.'
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserFacingError } from '@expo/eas-build-job/dist/errors';
|
||||
import { UserError } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { asyncResult } from '@expo/results';
|
||||
import {
|
||||
@@ -134,7 +134,7 @@ export async function downloadBuildAsync({
|
||||
});
|
||||
|
||||
if (matchingFiles.length === 0) {
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'EAS_DOWNLOAD_BUILD_NO_MATCHING_FILES',
|
||||
`No ${extensions.map(ext => `.${ext}`).join(', ')} entries found in the archive.`
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserFacingError } from '@expo/eas-build-job/dist/errors';
|
||||
import { UserError } from '@expo/eas-build-job';
|
||||
import {
|
||||
BuildFunction,
|
||||
BuildStepInput,
|
||||
@@ -51,10 +51,7 @@ export function createReadIpaInfoBuildFunction(): BuildFunction {
|
||||
const ipaPathInput = z.string().parse(inputs.ipa_path.value);
|
||||
const ipaPath = path.resolve(stepCtx.workingDirectory, ipaPathInput);
|
||||
if (!(await fs.pathExists(ipaPath))) {
|
||||
throw new UserFacingError(
|
||||
'EAS_READ_IPA_INFO_FILE_NOT_FOUND',
|
||||
`IPA file not found: ${ipaPath}`
|
||||
);
|
||||
throw new UserError('EAS_READ_IPA_INFO_FILE_NOT_FOUND', `IPA file not found: ${ipaPath}`);
|
||||
}
|
||||
|
||||
const ipaInfo = await readIpaInfoAsync(ipaPath);
|
||||
@@ -72,7 +69,7 @@ export async function readIpaInfoAsync(ipaPath: string): Promise<IpaInfo> {
|
||||
|
||||
const bundleIdentifier = infoPlist.CFBundleIdentifier;
|
||||
if (typeof bundleIdentifier !== 'string') {
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'EAS_READ_IPA_INFO_INVALID_INFO_PLIST',
|
||||
'Failed to read IPA info: Missing or invalid CFBundleIdentifier in Info.plist'
|
||||
);
|
||||
@@ -80,7 +77,7 @@ export async function readIpaInfoAsync(ipaPath: string): Promise<IpaInfo> {
|
||||
|
||||
const bundleShortVersion = infoPlist.CFBundleShortVersionString;
|
||||
if (typeof bundleShortVersion !== 'string') {
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'EAS_READ_IPA_INFO_INVALID_INFO_PLIST',
|
||||
'Failed to read IPA info: Missing or invalid CFBundleShortVersionString in Info.plist'
|
||||
);
|
||||
@@ -88,7 +85,7 @@ export async function readIpaInfoAsync(ipaPath: string): Promise<IpaInfo> {
|
||||
|
||||
const bundleVersion = infoPlist.CFBundleVersion;
|
||||
if (typeof bundleVersion !== 'string') {
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'EAS_READ_IPA_INFO_INVALID_INFO_PLIST',
|
||||
'Failed to read IPA info: Missing or invalid CFBundleVersion in Info.plist'
|
||||
);
|
||||
@@ -100,11 +97,11 @@ export async function readIpaInfoAsync(ipaPath: string): Promise<IpaInfo> {
|
||||
bundleVersion,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof UserFacingError) {
|
||||
if (error instanceof UserError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'EAS_READ_IPA_INFO_FAILED',
|
||||
`Failed to read IPA info: ${(error as Error).message}`,
|
||||
{ cause: error }
|
||||
@@ -118,10 +115,7 @@ function parseInfoPlistBuffer(data: Buffer): Record<string, unknown> {
|
||||
const parsedBinaryPlists = bplistParser.parseBuffer(data);
|
||||
const parsedBinaryPlist = parsedBinaryPlists[0];
|
||||
if (!parsedBinaryPlist || typeof parsedBinaryPlist !== 'object') {
|
||||
throw new UserFacingError(
|
||||
'EAS_READ_IPA_INFO_INVALID_BINARY_PLIST',
|
||||
'Invalid binary plist in IPA'
|
||||
);
|
||||
throw new UserError('EAS_READ_IPA_INFO_INVALID_BINARY_PLIST', 'Invalid binary plist in IPA');
|
||||
}
|
||||
return parsedBinaryPlist as Record<string, unknown>;
|
||||
}
|
||||
@@ -135,7 +129,7 @@ async function readInfoPlistBufferFromIpaAsync(ipaPath: string): Promise<Buffer>
|
||||
const entries = Object.values(await zip.entries());
|
||||
const infoPlistEntry = entries.find(entry => INFO_PLIST_PATH_REGEXP.test(entry.name));
|
||||
if (!infoPlistEntry) {
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'EAS_READ_IPA_INFO_INFO_PLIST_NOT_FOUND',
|
||||
`Failed to read IPA info: Could not find Info.plist in ${ipaPath}`
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserFacingError } from '@expo/eas-build-job/dist/errors';
|
||||
import { UserError } from '@expo/eas-build-job';
|
||||
import { asyncResult } from '@expo/results';
|
||||
import {
|
||||
BuildFunction,
|
||||
@@ -272,7 +272,7 @@ export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
? ipaInfoResult.value.bundleIdentifier
|
||||
: null;
|
||||
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'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` +
|
||||
@@ -284,7 +284,7 @@ export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
}
|
||||
if (isMissingPurposeStringError(errors)) {
|
||||
const missingUsageDescriptionKeys = parseMissingUsageDescriptionKeys(errors);
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'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` +
|
||||
`${
|
||||
@@ -303,7 +303,7 @@ export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
);
|
||||
}
|
||||
if (isClosedVersionTrainError(errors)) {
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'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. ' +
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function runGradleCommand({
|
||||
return line;
|
||||
}
|
||||
},
|
||||
env: { ...env, ...extraEnv },
|
||||
env: { ...env, ...extraEnv, LC_ALL: 'C.UTF-8' },
|
||||
});
|
||||
if (env.EAS_BUILD_RUNNER === 'eas-build' && process.platform === 'linux') {
|
||||
adjustOOMScore(spawnPromise, logger);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { templateString } from '@expo/template-file';
|
||||
import fs from 'fs-extra';
|
||||
@@ -6,6 +7,7 @@ import path from 'path';
|
||||
|
||||
import { EasBuildConfigureVersionGradleTemplate } from '../../../templates/EasBuildConfigureVersionGradle';
|
||||
import { EasBuildInjectAndroidCredentialsGradle } from '../../../templates/EasBuildInjectAndroidCredentialsGradle';
|
||||
import { BuildContext } from '../../../context';
|
||||
|
||||
const APPLY_EAS_BUILD_INJECT_CREDENTIALS_GRADLE_LINE =
|
||||
'apply from: "./eas-build-inject-android-credentials.gradle"';
|
||||
@@ -51,6 +53,26 @@ async function deleteEasBuildConfigureVersionGradle(workingDir: string): Promise
|
||||
await fs.remove(targetPath);
|
||||
}
|
||||
|
||||
let legacyEasBuildGradleWarningEmitted = false;
|
||||
|
||||
export async function warnIfLegacyEasBuildGradleExists(
|
||||
ctx: BuildContext<Android.Job>
|
||||
): Promise<void> {
|
||||
const legacyGradlePath = getLegacyEasBuildGradlePath(ctx.getReactNativeProjectDirectory());
|
||||
if (
|
||||
(await fs.pathExists(legacyGradlePath)) &&
|
||||
(process.env.NODE_ENV === 'test' || !legacyEasBuildGradleWarningEmitted)
|
||||
) {
|
||||
legacyEasBuildGradleWarningEmitted = true;
|
||||
ctx.logger.warn('eas-build.gradle script is deprecated, please remove it from your project.');
|
||||
ctx.markBuildPhaseHasWarnings();
|
||||
}
|
||||
}
|
||||
|
||||
function getLegacyEasBuildGradlePath(projectRoot: string): string {
|
||||
return path.join(projectRoot, 'android/app/eas-build.gradle');
|
||||
}
|
||||
|
||||
function getEasBuildInjectCredentialsGradlePath(workingDir: string): string {
|
||||
return path.join(workingDir, 'android/app/eas-build-inject-android-credentials.gradle');
|
||||
}
|
||||
@@ -113,7 +135,7 @@ function hasLine(haystack: string, needle: string): boolean {
|
||||
haystack
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
// Check for both single and double quotes
|
||||
// Check for both single and double quotes.
|
||||
.some(line => line === needle || line === needle.replace(/"/g, "'"))
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserFacingError } from '@expo/eas-build-job/dist/errors';
|
||||
import { UserError } from '@expo/eas-build-job';
|
||||
|
||||
import {
|
||||
AscApiClient,
|
||||
@@ -39,7 +39,7 @@ export namespace AscApiUtils {
|
||||
// Don't hide the original NOT_FOUND error with a secondary lookup failure.
|
||||
throw error;
|
||||
}
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'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.' +
|
||||
@@ -94,7 +94,7 @@ export namespace AscApiUtils {
|
||||
errors.every(item => item.code === 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE');
|
||||
|
||||
if (isDuplicateVersionError) {
|
||||
throw new UserFacingError(
|
||||
throw new UserError(
|
||||
'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). ' +
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('AscApiUtils', () => {
|
||||
).resolves.toEqual(response);
|
||||
});
|
||||
|
||||
it('throws UserFacingError with visible apps when app id is not found', async () => {
|
||||
it('throws UserError with visible apps when app id is not found', async () => {
|
||||
const notFoundPayload = {
|
||||
errors: [
|
||||
{
|
||||
@@ -97,7 +97,7 @@ describe('AscApiUtils', () => {
|
||||
});
|
||||
|
||||
describe('createBuildUploadAsync', () => {
|
||||
it('throws UserFacingError when ASC duplicate version error is returned', async () => {
|
||||
it('throws UserError when ASC duplicate version error is returned', async () => {
|
||||
const payload = {
|
||||
errors: [
|
||||
{
|
||||
|
||||
@@ -42,6 +42,7 @@ export default class ProvisioningProfile {
|
||||
|
||||
private readonly profilePath: string;
|
||||
private profileData?: ProvisioningProfileData;
|
||||
private developerCertificates: Buffer[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly profile: Buffer,
|
||||
@@ -73,11 +74,12 @@ export default class ProvisioningProfile {
|
||||
}
|
||||
|
||||
public verifyCertificate(fingerprint: string): void {
|
||||
const devCertFingerprint = this.genDerCertFingerprint();
|
||||
if (devCertFingerprint !== fingerprint) {
|
||||
const devCertFingerprints = this.getAllDerCertFingerprints();
|
||||
if (!devCertFingerprints.includes(fingerprint)) {
|
||||
throw new errors.CredentialsDistCertMismatchError(
|
||||
`Provisioning profile and distribution certificate don't match.
|
||||
Profile's certificate fingerprint = ${devCertFingerprint}, distribution certificate fingerprint = ${fingerprint}`
|
||||
`Provisioning profile and distribution certificate don't match.\n` +
|
||||
`Profile's certificate fingerprints = [${devCertFingerprints.join(', ')}], ` +
|
||||
`distribution certificate fingerprint = ${fingerprint}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -110,6 +112,10 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
|
||||
] as string;
|
||||
const bundleIdentifier = applicationIdentifier.replace(/^.+?\./, '');
|
||||
|
||||
this.developerCertificates = (plistData.DeveloperCertificates as string[]).map((cert: string) =>
|
||||
Buffer.from(cert, 'base64')
|
||||
);
|
||||
|
||||
this.profileData = {
|
||||
path: this.profilePath,
|
||||
target: this.target,
|
||||
@@ -117,7 +123,7 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
|
||||
teamId: (plistData.TeamIdentifier as string[])[0],
|
||||
uuid: plistData.UUID as string,
|
||||
name: plistData.Name as string,
|
||||
developerCertificate: Buffer.from((plistData.DeveloperCertificates as string[])[0], 'base64'),
|
||||
developerCertificate: this.developerCertificates[0],
|
||||
certificateCommonName: this.certificateCommonName,
|
||||
distributionType: this.resolveDistributionType(plistData),
|
||||
};
|
||||
@@ -133,11 +139,9 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
|
||||
}
|
||||
}
|
||||
|
||||
private genDerCertFingerprint(): string {
|
||||
return crypto
|
||||
.createHash('sha1')
|
||||
.update(new Uint8Array(this.data.developerCertificate))
|
||||
.digest('hex')
|
||||
.toUpperCase();
|
||||
private getAllDerCertFingerprints(): string[] {
|
||||
return this.developerCertificates.map(cert =>
|
||||
crypto.createHash('sha1').update(new Uint8Array(cert)).digest('hex').toUpperCase()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
export const EasBuildGradle = `// Build integration with EAS
|
||||
|
||||
import java.nio.file.Paths
|
||||
|
||||
def versionCodeFromEnv = System.getenv("EAS_BUILD_ANDROID_VERSION_CODE")
|
||||
def versionNameFromEnv = System.getenv("EAS_BUILD_ANDROID_VERSION_NAME")
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
if (versionCodeFromEnv) {
|
||||
versionCode = Integer.parseInt(versionCodeFromEnv)
|
||||
}
|
||||
if (versionNameFromEnv) {
|
||||
versionName = versionNameFromEnv
|
||||
}
|
||||
}
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
if (versionCodeFromEnv) {
|
||||
output.versionCodeOverride = Integer.parseInt(versionCodeFromEnv)
|
||||
}
|
||||
if (versionNameFromEnv) {
|
||||
output.versionNameOverride = versionNameFromEnv
|
||||
}
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
def credentialsJson = Paths.get(System.getenv("EAS_BUILD_WORKINGDIR")).resolve("credentials.json").toFile();
|
||||
def credentials = new groovy.json.JsonSlurper().parse(credentialsJson)
|
||||
def keystorePath = Paths.get(credentials.android.keystore.keystorePath);
|
||||
|
||||
storeFile keystorePath.toFile()
|
||||
storePassword credentials.android.keystore.keystorePassword
|
||||
keyAlias credentials.android.keystore.keyAlias
|
||||
if (credentials.android.keystore.containsKey("keyPassword")) {
|
||||
keyPassword credentials.android.keystore.keyPassword
|
||||
} else {
|
||||
// key password is required by Gradle, but PKCS keystores don't have one
|
||||
// using the keystore password seems to satisfy the requirement
|
||||
keyPassword credentials.android.keystore.keystorePassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig android.signingConfigs.release
|
||||
}
|
||||
debug {
|
||||
signingConfig android.signingConfigs.release
|
||||
}
|
||||
}
|
||||
}`;
|
||||
@@ -1,10 +1,13 @@
|
||||
import { StepMetricsCollection } from '@expo/steps';
|
||||
import { StepMetric } from '@expo/steps';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
import { uploadStepMetricsToWwwAsync } from '../stepMetrics';
|
||||
import { turtleFetch } from '../turtleFetch';
|
||||
|
||||
jest.mock('../turtleFetch');
|
||||
jest.mock('../turtleFetch', () => ({
|
||||
...jest.requireActual('../turtleFetch'),
|
||||
turtleFetch: jest.fn(),
|
||||
}));
|
||||
const mockTurtleFetch = turtleFetch as jest.MockedFunction<typeof turtleFetch>;
|
||||
|
||||
describe(uploadStepMetricsToWwwAsync, () => {
|
||||
@@ -23,44 +26,52 @@ describe(uploadStepMetricsToWwwAsync, () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('uploads step metrics to www', async () => {
|
||||
const stepMetrics: StepMetricsCollection = [
|
||||
{ metricsId: 'eas/checkout', result: 'success', durationMs: 1000, platform: 'linux' },
|
||||
{
|
||||
metricsId: 'eas/install_node_modules',
|
||||
result: 'failed',
|
||||
durationMs: 5000,
|
||||
platform: 'linux',
|
||||
},
|
||||
];
|
||||
it('uploads step metrics', async () => {
|
||||
const stepMetric: StepMetric = {
|
||||
metricsId: 'eas/checkout',
|
||||
result: 'success',
|
||||
durationMs: 1000,
|
||||
platform: 'linux',
|
||||
};
|
||||
|
||||
await uploadStepMetricsToWwwAsync({
|
||||
workflowJobId,
|
||||
robotAccessToken,
|
||||
expoApiV2BaseUrl,
|
||||
stepMetrics,
|
||||
stepMetrics: [stepMetric],
|
||||
logger: mockLogger,
|
||||
});
|
||||
|
||||
expect(mockTurtleFetch).toHaveBeenCalledTimes(1);
|
||||
expect(mockTurtleFetch).toHaveBeenCalledWith(
|
||||
`${expoApiV2BaseUrl}workflows/${workflowJobId}/metrics`,
|
||||
'POST',
|
||||
expect.objectContaining({
|
||||
json: { stepMetrics },
|
||||
json: { stepMetrics: [stepMetric] },
|
||||
headers: { Authorization: `Bearer ${robotAccessToken}` },
|
||||
retries: 2,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('does nothing when stepMetrics is empty', async () => {
|
||||
await uploadStepMetricsToWwwAsync({
|
||||
workflowJobId,
|
||||
robotAccessToken,
|
||||
expoApiV2BaseUrl,
|
||||
stepMetrics: [],
|
||||
logger: mockLogger,
|
||||
});
|
||||
it('silently gives up on failure', async () => {
|
||||
mockTurtleFetch.mockRejectedValue(new Error('persistent failure'));
|
||||
|
||||
expect(mockTurtleFetch).not.toHaveBeenCalled();
|
||||
const stepMetric: StepMetric = {
|
||||
metricsId: 'eas/checkout',
|
||||
result: 'failed',
|
||||
durationMs: 500,
|
||||
platform: 'darwin',
|
||||
};
|
||||
|
||||
await expect(
|
||||
uploadStepMetricsToWwwAsync({
|
||||
workflowJobId,
|
||||
robotAccessToken,
|
||||
expoApiV2BaseUrl,
|
||||
stepMetrics: [stepMetric],
|
||||
logger: mockLogger,
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { StepMetricsCollection } from '@expo/steps';
|
||||
import { StepMetric } from '@expo/steps';
|
||||
|
||||
import { turtleFetch } from './turtleFetch';
|
||||
|
||||
@@ -13,29 +13,22 @@ export async function uploadStepMetricsToWwwAsync({
|
||||
workflowJobId: string;
|
||||
robotAccessToken: string;
|
||||
expoApiV2BaseUrl: string;
|
||||
stepMetrics: StepMetricsCollection;
|
||||
stepMetrics: StepMetric[];
|
||||
logger: bunyan;
|
||||
}): Promise<void> {
|
||||
if (stepMetrics.length === 0) {
|
||||
logger.debug('No step metrics to upload');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await turtleFetch(
|
||||
new URL(`workflows/${workflowJobId}/metrics`, expoApiV2BaseUrl).toString(),
|
||||
'POST',
|
||||
{
|
||||
json: { stepMetrics },
|
||||
headers: {
|
||||
Authorization: `Bearer ${robotAccessToken}`,
|
||||
},
|
||||
timeout: 20000,
|
||||
headers: { Authorization: `Bearer ${robotAccessToken}` },
|
||||
timeout: 5000,
|
||||
retries: 2,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
logger.info(`Uploaded ${stepMetrics.length} step metrics`);
|
||||
} catch {
|
||||
// Don't display unactionable error to the user, let's send it to Sentry in the future
|
||||
// Don't fail the build for metrics — silently give up
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BuildError, ExpoError, UserFacingError } from '../errors';
|
||||
import { BuildError, ExpoError, UserError } from '../errors';
|
||||
import { BuildPhase } from '../logs';
|
||||
|
||||
class TestExpoError extends ExpoError {
|
||||
@@ -56,9 +56,9 @@ describe(BuildError, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(UserFacingError, () => {
|
||||
describe(UserError, () => {
|
||||
it('supports docsUrl in options', () => {
|
||||
const error = new UserFacingError('ERR_CODE', 'message', {
|
||||
const error = new UserError('ERR_CODE', 'message', {
|
||||
docsUrl: 'https://docs.example.dev',
|
||||
});
|
||||
|
||||
@@ -69,14 +69,14 @@ describe(UserFacingError, () => {
|
||||
|
||||
it('supports cause in options', () => {
|
||||
const cause = new Error('root cause');
|
||||
const error = new UserFacingError('ERR_CODE', 'message', { cause });
|
||||
const error = new UserError('ERR_CODE', 'message', { cause });
|
||||
|
||||
expect(error.cause).toBe(cause);
|
||||
});
|
||||
|
||||
it('supports docsUrl, metadata, and cause in options', () => {
|
||||
const cause = new Error('root cause');
|
||||
const error = new UserFacingError('ERR_CODE', 'message', {
|
||||
const error = new UserError('ERR_CODE', 'message', {
|
||||
docsUrl: 'https://docs.example.dev',
|
||||
metadata: { packageName: 'expo' },
|
||||
cause,
|
||||
|
||||
@@ -72,7 +72,7 @@ export class BuildError<
|
||||
}
|
||||
}
|
||||
|
||||
export class UserFacingError<
|
||||
export class UserError<
|
||||
TMetadata extends ErrorMetadata = ErrorMetadata,
|
||||
> extends ExpoError<TMetadata> {
|
||||
constructor(
|
||||
@@ -93,7 +93,7 @@ export class UserFacingError<
|
||||
}
|
||||
}
|
||||
|
||||
export class UnknownError extends UserFacingError {
|
||||
export class UnknownError extends UserError {
|
||||
constructor(buildPhase?: BuildPhase) {
|
||||
super(
|
||||
ErrorCode.UNKNOWN_ERROR,
|
||||
@@ -124,7 +124,7 @@ export class UnknownCustomBuildError extends BuildError {
|
||||
}
|
||||
}
|
||||
|
||||
export class CredentialsDistCertMismatchError extends UserFacingError {
|
||||
export class CredentialsDistCertMismatchError extends UserError {
|
||||
constructor(message: string) {
|
||||
super('EAS_BUILD_CREDENTIALS_DIST_CERT_MISMATCH', message);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export {
|
||||
export { Metadata, sanitizeMetadata } from './metadata';
|
||||
export * from './job';
|
||||
export * from './logs';
|
||||
export * from './errors';
|
||||
export * as errors from './errors';
|
||||
export * from './artifacts';
|
||||
export * from './context';
|
||||
|
||||
+88
-88
@@ -178,7 +178,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -195,7 +195,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:usage [ACCOUNT_NAME]`
|
||||
|
||||
@@ -217,7 +217,7 @@ DESCRIPTION
|
||||
view account usage and billing for the current cycle
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -234,7 +234,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -248,7 +248,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -300,7 +300,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -321,7 +321,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -341,7 +341,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -361,7 +361,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -384,7 +384,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -422,7 +422,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -442,7 +442,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -460,7 +460,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:delete [BUILD_ID]`
|
||||
|
||||
@@ -480,7 +480,7 @@ DESCRIPTION
|
||||
delete a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
|
||||
## `eas build:dev`
|
||||
|
||||
@@ -500,7 +500,7 @@ DESCRIPTION
|
||||
run dev client simulator/emulator build with matching fingerprint or create a new one
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
|
||||
## `eas build:download`
|
||||
|
||||
@@ -521,7 +521,7 @@ DESCRIPTION
|
||||
download simulator/emulator builds for a given fingerprint hash
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -559,7 +559,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -599,7 +599,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -629,7 +629,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -655,7 +655,7 @@ DESCRIPTION
|
||||
run simulator/emulator builds from eas-cli
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -707,7 +707,7 @@ DESCRIPTION
|
||||
get the latest version from EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
|
||||
## `eas build:version:set`
|
||||
|
||||
@@ -725,7 +725,7 @@ DESCRIPTION
|
||||
update version of an app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -743,7 +743,7 @@ DESCRIPTION
|
||||
update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -760,7 +760,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -781,7 +781,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:delete [NAME]`
|
||||
|
||||
@@ -802,7 +802,7 @@ DESCRIPTION
|
||||
Delete a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -824,7 +824,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -844,7 +844,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:pause [NAME]`
|
||||
|
||||
@@ -866,7 +866,7 @@ DESCRIPTION
|
||||
pause a channel to stop it from sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
|
||||
## `eas channel:resume [NAME]`
|
||||
|
||||
@@ -888,7 +888,7 @@ DESCRIPTION
|
||||
resume a channel to start sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
@@ -923,7 +923,7 @@ DESCRIPTION
|
||||
Roll a new branch out on a channel incrementally.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -946,7 +946,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -967,7 +967,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -984,7 +984,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
|
||||
## `eas credentials:configure-build`
|
||||
|
||||
@@ -1002,7 +1002,7 @@ DESCRIPTION
|
||||
Set up credentials for building your project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
|
||||
## `eas deploy [options]`
|
||||
|
||||
@@ -1031,7 +1031,7 @@ ALIASES
|
||||
$ eas worker:deploy
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
|
||||
## `eas deploy:alias`
|
||||
|
||||
@@ -1056,7 +1056,7 @@ ALIASES
|
||||
$ eas deploy:promote
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
|
||||
## `eas deploy:alias:delete [ALIAS_NAME]`
|
||||
|
||||
@@ -1077,7 +1077,7 @@ ALIASES
|
||||
$ eas worker:alias:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
|
||||
## `eas deploy:delete [DEPLOYMENT_ID]`
|
||||
|
||||
@@ -1098,7 +1098,7 @@ ALIASES
|
||||
$ eas worker:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
|
||||
## `eas deploy:promote`
|
||||
|
||||
@@ -1135,7 +1135,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -1155,7 +1155,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -1176,7 +1176,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:rename`
|
||||
|
||||
@@ -1197,7 +1197,7 @@ DESCRIPTION
|
||||
rename a registered device
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -1211,7 +1211,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -1225,7 +1225,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas env:create [ENVIRONMENT]`
|
||||
|
||||
@@ -1257,7 +1257,7 @@ DESCRIPTION
|
||||
create an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
|
||||
## `eas env:delete [ENVIRONMENT]`
|
||||
|
||||
@@ -1283,7 +1283,7 @@ DESCRIPTION
|
||||
delete an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
|
||||
## `eas env:exec ENVIRONMENT BASH_COMMAND`
|
||||
|
||||
@@ -1305,7 +1305,7 @@ DESCRIPTION
|
||||
execute a command with environment variables from the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
|
||||
## `eas env:get [ENVIRONMENT]`
|
||||
|
||||
@@ -1333,7 +1333,7 @@ DESCRIPTION
|
||||
view an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
|
||||
## `eas env:list [ENVIRONMENT]`
|
||||
|
||||
@@ -1361,7 +1361,7 @@ DESCRIPTION
|
||||
list environment variables for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
|
||||
## `eas env:pull [ENVIRONMENT]`
|
||||
|
||||
@@ -1384,7 +1384,7 @@ DESCRIPTION
|
||||
pull environment variables for the selected environment to .env file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
|
||||
## `eas env:push [ENVIRONMENT]`
|
||||
|
||||
@@ -1406,7 +1406,7 @@ DESCRIPTION
|
||||
push environment variables from .env file to the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
|
||||
## `eas env:update [ENVIRONMENT]`
|
||||
|
||||
@@ -1440,7 +1440,7 @@ DESCRIPTION
|
||||
update an environment variable on the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
|
||||
## `eas fingerprint:compare [HASH1] [HASH2]`
|
||||
|
||||
@@ -1484,7 +1484,7 @@ EXAMPLES
|
||||
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
|
||||
## `eas fingerprint:generate`
|
||||
|
||||
@@ -1515,7 +1515,7 @@ EXAMPLES
|
||||
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -1624,7 +1624,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -1641,7 +1641,7 @@ DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -1658,7 +1658,7 @@ DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas new [PATH]`
|
||||
|
||||
@@ -1710,7 +1710,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -1724,7 +1724,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -1747,7 +1747,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas project:new [PATH]`
|
||||
|
||||
@@ -1771,7 +1771,7 @@ ALIASES
|
||||
$ eas new
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
|
||||
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
|
||||
|
||||
@@ -1789,7 +1789,7 @@ ALIASES
|
||||
$ eas onboarding
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -1822,7 +1822,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1865,7 +1865,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1886,7 +1886,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1907,7 +1907,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:edit [GROUPID]`
|
||||
|
||||
@@ -1932,7 +1932,7 @@ DESCRIPTION
|
||||
edit all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -1959,7 +1959,7 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
@@ -1997,7 +1997,7 @@ DESCRIPTION
|
||||
roll back to an existing update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:revert-update-rollout`
|
||||
|
||||
@@ -2025,7 +2025,7 @@ DESCRIPTION
|
||||
revert a rollout update for a project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
|
||||
## `eas update:roll-back-to-embedded`
|
||||
|
||||
@@ -2055,7 +2055,7 @@ DESCRIPTION
|
||||
roll back to the embedded update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
|
||||
## `eas update:rollback`
|
||||
|
||||
@@ -2076,7 +2076,7 @@ DESCRIPTION
|
||||
instead execute "eas update:republish" or "eas update:roll-back-to-embedded".
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -2096,7 +2096,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas upload`
|
||||
|
||||
@@ -2117,7 +2117,7 @@ DESCRIPTION
|
||||
upload a local build and generate a sharable link
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -2139,7 +2139,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -2159,7 +2159,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -2178,7 +2178,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -2201,7 +2201,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -2218,7 +2218,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
@@ -2339,7 +2339,7 @@ DESCRIPTION
|
||||
to cancel.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
|
||||
## `eas workflow:create [NAME]`
|
||||
|
||||
@@ -2359,7 +2359,7 @@ DESCRIPTION
|
||||
create a new workflow configuration YAML file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
|
||||
## `eas workflow:logs [ID]`
|
||||
|
||||
@@ -2383,7 +2383,7 @@ DESCRIPTION
|
||||
If no ID is passed in, you will be prompted to select from recent workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
|
||||
## `eas workflow:run FILE`
|
||||
|
||||
@@ -2423,7 +2423,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
|
||||
## `eas workflow:runs`
|
||||
|
||||
@@ -2445,7 +2445,7 @@ DESCRIPTION
|
||||
list recent workflow runs for this project, with their IDs, statuses, and timestamps
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
|
||||
## `eas workflow:status [WORKFLOW_RUN_ID]`
|
||||
|
||||
@@ -2473,7 +2473,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
|
||||
## `eas workflow:validate PATH`
|
||||
|
||||
@@ -2493,7 +2493,7 @@ DESCRIPTION
|
||||
validate a workflow configuration yaml file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
|
||||
## `eas workflow:view [ID]`
|
||||
|
||||
@@ -2515,5 +2515,5 @@ DESCRIPTION
|
||||
workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.2.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.3.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "18.2.0",
|
||||
"version": "18.3.0",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
+1
@@ -37,6 +37,7 @@ describe(getProjectIdAsync, () => {
|
||||
actor: {
|
||||
__typename: 'User',
|
||||
id: 'user_id',
|
||||
email: '[email protected]',
|
||||
username: 'notnotbrent',
|
||||
primaryAccount: {
|
||||
id: 'account_id_1',
|
||||
|
||||
@@ -19,10 +19,14 @@ export default class AccountView extends EasCommand {
|
||||
maybeLoggedIn: { actor, authenticationInfo },
|
||||
} = await this.getContextAsync(AccountView, { nonInteractive: true });
|
||||
if (actor) {
|
||||
const displayName = getActorDisplayName(actor);
|
||||
const loggedInAs = authenticationInfo.accessToken
|
||||
? `${getActorDisplayName(actor)} (authenticated using EXPO_TOKEN)`
|
||||
: getActorDisplayName(actor);
|
||||
? `${displayName} (authenticated using EXPO_TOKEN)`
|
||||
: displayName;
|
||||
Log.log(chalk.green(loggedInAs));
|
||||
if ('email' in actor) {
|
||||
Log.log(actor.email);
|
||||
}
|
||||
|
||||
// personal account is included, only show if more accounts that personal account
|
||||
// but do show personal account in list if there are more
|
||||
|
||||
@@ -43,6 +43,10 @@ export default class BuildDev extends EasCommand {
|
||||
description: `Name of the build profile from eas.json. It must be a profile allowing to create emulator/simulator internal distribution dev client builds. The "${DEFAULT_EAS_BUILD_RUN_PROFILE_NAME}" build profile will be selected by default.`,
|
||||
helpValue: 'PROFILE_NAME',
|
||||
}),
|
||||
'skip-build-if-not-found': Flags.boolean({
|
||||
description: 'Skip build if no successful build with matching fingerprint is found.',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
@@ -128,10 +132,15 @@ export default class BuildDev extends EasCommand {
|
||||
await this.startDevServerAsync({ projectDir, platform });
|
||||
return;
|
||||
} else {
|
||||
Log.warn('Artifacts for this build expired. New build will be started.');
|
||||
Log.warn('Artifacts for this build expired.');
|
||||
}
|
||||
}
|
||||
|
||||
if (flags['skip-build-if-not-found']) {
|
||||
Log.log('No successful build with matching fingerprint found. Skipping build.');
|
||||
return;
|
||||
}
|
||||
|
||||
Log.log('🚀 No successful build with matching fingerprint found. Starting a new build...');
|
||||
|
||||
const previousBuildsForSelectedProfile = await this.getBuildsAsync({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Actor } from '../../user/User';
|
||||
export const jester: Actor = {
|
||||
__typename: 'User' as const,
|
||||
id: 'jester-id',
|
||||
email: '[email protected]',
|
||||
username: 'jester',
|
||||
primaryAccount: {
|
||||
id: 'jester-account-id',
|
||||
|
||||
@@ -24,6 +24,7 @@ describe(AccountResolver, () => {
|
||||
const user: Actor = {
|
||||
__typename: 'User',
|
||||
id: 'user_id_666',
|
||||
email: '[email protected]',
|
||||
username: 'dominik',
|
||||
primaryAccount: {
|
||||
id: 'account_id_777',
|
||||
|
||||
+1
@@ -16519,6 +16519,7 @@ export type CurrentUserQuery = {
|
||||
}
|
||||
| {
|
||||
__typename: 'User';
|
||||
email: string;
|
||||
username: string;
|
||||
id: string;
|
||||
featureGates: any;
|
||||
|
||||
@@ -16,6 +16,9 @@ export const UserQuery = {
|
||||
meActor {
|
||||
__typename
|
||||
id
|
||||
... on User {
|
||||
email
|
||||
}
|
||||
... on UserActor {
|
||||
username
|
||||
primaryAccount {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Actor, getActorDisplayName } from '../User';
|
||||
const userStub: Actor = {
|
||||
__typename: 'User',
|
||||
id: 'userId',
|
||||
email: '[email protected]',
|
||||
username: 'username',
|
||||
primaryAccount: {
|
||||
id: 'account_id_777',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { BuildRuntimePlatform } from './BuildRuntimePlatform';
|
||||
import { BuildStep, BuildStepOutputAccessor, SerializedBuildStepOutputAccessor } from './BuildStep';
|
||||
import { BuildStepEnv } from './BuildStepEnv';
|
||||
import { StepMetricInput, StepMetricsCollection } from './StepMetrics';
|
||||
import { StepMetric, StepMetricInput } from './StepMetrics';
|
||||
import { BuildStepRuntimeError } from './errors';
|
||||
import { hashFiles } from './utils/hashFiles';
|
||||
import {
|
||||
@@ -40,6 +40,7 @@ export interface ExternalBuildContextProvider {
|
||||
|
||||
readonly env: BuildStepEnv;
|
||||
updateEnv(env: BuildStepEnv): void;
|
||||
reportStepMetric?(metric: StepMetric): void;
|
||||
}
|
||||
|
||||
export interface SerializedBuildStepGlobalContext {
|
||||
@@ -56,8 +57,6 @@ export class BuildStepGlobalContext {
|
||||
private didCheckOut = false;
|
||||
private _hasAnyPreviousStepFailed = false;
|
||||
private stepById: Record<string, BuildStepOutputAccessor> = {};
|
||||
private readonly _stepMetrics: StepMetricsCollection = [];
|
||||
|
||||
constructor(
|
||||
private readonly provider: ExternalBuildContextProvider,
|
||||
public readonly skipCleanup: boolean
|
||||
@@ -183,12 +182,9 @@ export class BuildStepGlobalContext {
|
||||
this._hasAnyPreviousStepFailed = true;
|
||||
}
|
||||
|
||||
public get stepMetrics(): StepMetricsCollection {
|
||||
return this._stepMetrics;
|
||||
}
|
||||
|
||||
public addStepMetric(metric: StepMetricInput): void {
|
||||
this._stepMetrics.push({ ...metric, platform: this.runtimePlatform });
|
||||
const stepMetric: StepMetric = { ...metric, platform: this.runtimePlatform };
|
||||
this.provider.reportStepMetric?.(stepMetric);
|
||||
}
|
||||
|
||||
public wasCheckedOut(): boolean {
|
||||
|
||||
@@ -9,5 +9,3 @@ export type StepMetricInput = {
|
||||
export type StepMetric = StepMetricInput & {
|
||||
platform: 'darwin' | 'linux';
|
||||
};
|
||||
|
||||
export type StepMetricsCollection = StepMetric[];
|
||||
@@ -4,6 +4,7 @@ import { createGlobalContextMock } from './utils/context';
|
||||
import { BuildRuntimePlatform } from '../BuildRuntimePlatform';
|
||||
import { BuildStep } from '../BuildStep';
|
||||
import { BuildWorkflow } from '../BuildWorkflow';
|
||||
import { StepMetric } from '../StepMetrics';
|
||||
|
||||
describe(BuildWorkflow, () => {
|
||||
describe(BuildWorkflow.prototype.executeAsync, () => {
|
||||
@@ -159,6 +160,19 @@ describe(BuildWorkflow, () => {
|
||||
});
|
||||
|
||||
describe('step metrics collection', () => {
|
||||
function createContextWithMetricCollector(
|
||||
params: { runtimePlatform?: BuildRuntimePlatform } = {}
|
||||
): { ctx: ReturnType<typeof createGlobalContextMock>; collected: StepMetric[] } {
|
||||
const collected: StepMetric[] = [];
|
||||
const ctx = createGlobalContextMock({
|
||||
...params,
|
||||
reportStepMetric: metric => {
|
||||
collected.push(metric);
|
||||
},
|
||||
});
|
||||
return { ctx, collected };
|
||||
}
|
||||
|
||||
it('collects metrics for steps with __metricsId', async () => {
|
||||
const mockBuildStep = mock<BuildStep>();
|
||||
when(mockBuildStep.shouldExecuteStep()).thenReturn(true);
|
||||
@@ -167,17 +181,19 @@ describe(BuildWorkflow, () => {
|
||||
|
||||
const buildSteps: BuildStep[] = [instance(mockBuildStep)];
|
||||
|
||||
const ctx = createGlobalContextMock({ runtimePlatform: BuildRuntimePlatform.LINUX });
|
||||
const { ctx, collected } = createContextWithMetricCollector({
|
||||
runtimePlatform: BuildRuntimePlatform.LINUX,
|
||||
});
|
||||
const workflow = new BuildWorkflow(ctx, { buildSteps, buildFunctions: {} });
|
||||
await workflow.executeAsync();
|
||||
|
||||
expect(ctx.stepMetrics).toHaveLength(1);
|
||||
expect(ctx.stepMetrics[0]).toMatchObject({
|
||||
expect(collected).toHaveLength(1);
|
||||
expect(collected[0]).toMatchObject({
|
||||
metricsId: 'test-step-metrics',
|
||||
result: 'success',
|
||||
platform: 'linux',
|
||||
});
|
||||
expect(ctx.stepMetrics[0].durationMs).toBeGreaterThanOrEqual(0);
|
||||
expect(collected[0].durationMs).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('does not collect metrics for steps without __metricsId', async () => {
|
||||
@@ -188,11 +204,11 @@ describe(BuildWorkflow, () => {
|
||||
|
||||
const buildSteps: BuildStep[] = [instance(mockBuildStep)];
|
||||
|
||||
const ctx = createGlobalContextMock();
|
||||
const { ctx, collected } = createContextWithMetricCollector();
|
||||
const workflow = new BuildWorkflow(ctx, { buildSteps, buildFunctions: {} });
|
||||
await workflow.executeAsync();
|
||||
|
||||
expect(ctx.stepMetrics).toHaveLength(0);
|
||||
expect(collected).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('collects failed result when step throws', async () => {
|
||||
@@ -203,17 +219,19 @@ describe(BuildWorkflow, () => {
|
||||
|
||||
const buildSteps: BuildStep[] = [instance(mockBuildStep)];
|
||||
|
||||
const ctx = createGlobalContextMock({ runtimePlatform: BuildRuntimePlatform.DARWIN });
|
||||
const { ctx, collected } = createContextWithMetricCollector({
|
||||
runtimePlatform: BuildRuntimePlatform.DARWIN,
|
||||
});
|
||||
const workflow = new BuildWorkflow(ctx, { buildSteps, buildFunctions: {} });
|
||||
await expect(workflow.executeAsync()).rejects.toThrowError('Step failed');
|
||||
|
||||
expect(ctx.stepMetrics).toHaveLength(1);
|
||||
expect(ctx.stepMetrics[0]).toMatchObject({
|
||||
expect(collected).toHaveLength(1);
|
||||
expect(collected[0]).toMatchObject({
|
||||
metricsId: 'failing-step-metrics',
|
||||
result: 'failed',
|
||||
platform: 'darwin',
|
||||
});
|
||||
expect(ctx.stepMetrics[0].durationMs).toBeGreaterThanOrEqual(0);
|
||||
expect(collected[0].durationMs).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('does not collect metrics when step is skipped', async () => {
|
||||
@@ -223,13 +241,13 @@ describe(BuildWorkflow, () => {
|
||||
|
||||
const buildSteps: BuildStep[] = [instance(mockBuildStep)];
|
||||
|
||||
const ctx = createGlobalContextMock();
|
||||
const { ctx, collected } = createContextWithMetricCollector();
|
||||
const workflow = new BuildWorkflow(ctx, { buildSteps, buildFunctions: {} });
|
||||
await workflow.executeAsync();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
||||
verify(mockBuildStep.skip()).once();
|
||||
expect(ctx.stepMetrics).toHaveLength(0);
|
||||
expect(collected).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('collects metrics with darwin platform when runtimePlatform is darwin', async () => {
|
||||
@@ -240,12 +258,14 @@ describe(BuildWorkflow, () => {
|
||||
|
||||
const buildSteps: BuildStep[] = [instance(mockBuildStep)];
|
||||
|
||||
const ctx = createGlobalContextMock({ runtimePlatform: BuildRuntimePlatform.DARWIN });
|
||||
const { ctx, collected } = createContextWithMetricCollector({
|
||||
runtimePlatform: BuildRuntimePlatform.DARWIN,
|
||||
});
|
||||
const workflow = new BuildWorkflow(ctx, { buildSteps, buildFunctions: {} });
|
||||
await workflow.executeAsync();
|
||||
|
||||
expect(ctx.stepMetrics).toHaveLength(1);
|
||||
expect(ctx.stepMetrics[0].platform).toBe('darwin');
|
||||
expect(collected).toHaveLength(1);
|
||||
expect(collected[0].platform).toBe('darwin');
|
||||
});
|
||||
|
||||
it('collects metrics for multiple steps', async () => {
|
||||
@@ -271,13 +291,27 @@ describe(BuildWorkflow, () => {
|
||||
instance(mockBuildStep3),
|
||||
];
|
||||
|
||||
const ctx = createGlobalContextMock({ runtimePlatform: BuildRuntimePlatform.LINUX });
|
||||
const { ctx, collected } = createContextWithMetricCollector({
|
||||
runtimePlatform: BuildRuntimePlatform.LINUX,
|
||||
});
|
||||
const workflow = new BuildWorkflow(ctx, { buildSteps, buildFunctions: {} });
|
||||
await workflow.executeAsync();
|
||||
|
||||
expect(ctx.stepMetrics).toHaveLength(2);
|
||||
expect(ctx.stepMetrics[0].metricsId).toBe('step-1');
|
||||
expect(ctx.stepMetrics[1].metricsId).toBe('step-3');
|
||||
expect(collected).toHaveLength(2);
|
||||
expect(collected[0].metricsId).toBe('step-1');
|
||||
expect(collected[1].metricsId).toBe('step-3');
|
||||
});
|
||||
|
||||
it('does not throw when reportStepMetric is not provided on the provider', async () => {
|
||||
const mockBuildStep = mock<BuildStep>();
|
||||
when(mockBuildStep.shouldExecuteStep()).thenReturn(true);
|
||||
when(mockBuildStep.__metricsId).thenReturn('no-callback-test');
|
||||
|
||||
const buildSteps: BuildStep[] = [instance(mockBuildStep)];
|
||||
const ctx = createGlobalContextMock();
|
||||
|
||||
const workflow = new BuildWorkflow(ctx, { buildSteps, buildFunctions: {} });
|
||||
await expect(workflow.executeAsync()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
ExternalBuildContextProvider,
|
||||
} from '../../BuildStepContext';
|
||||
import { BuildStepEnv } from '../../BuildStepEnv';
|
||||
import { StepMetric } from '../../StepMetrics';
|
||||
|
||||
export class MockContextProvider implements ExternalBuildContextProvider {
|
||||
private _env: BuildStepEnv = {};
|
||||
public reportStepMetric?: (metric: StepMetric) => void;
|
||||
|
||||
constructor(
|
||||
public readonly logger: bunyan,
|
||||
@@ -45,6 +47,7 @@ interface BuildContextParams {
|
||||
projectTargetDirectory?: string;
|
||||
relativeWorkingDirectory?: string;
|
||||
staticContextContent?: JobInterpolationContext;
|
||||
reportStepMetric?: (metric: StepMetric) => void;
|
||||
}
|
||||
|
||||
export function createStepContextMock({
|
||||
@@ -81,21 +84,23 @@ export function createGlobalContextMock({
|
||||
projectTargetDirectory,
|
||||
relativeWorkingDirectory,
|
||||
staticContextContent,
|
||||
reportStepMetric,
|
||||
}: BuildContextParams = {}): BuildStepGlobalContext {
|
||||
const resolvedProjectTargetDirectory =
|
||||
projectTargetDirectory ?? path.join(os.tmpdir(), 'eas-build', uuidv4());
|
||||
return new BuildStepGlobalContext(
|
||||
new MockContextProvider(
|
||||
logger ?? createMockLogger(),
|
||||
runtimePlatform ?? BuildRuntimePlatform.LINUX,
|
||||
projectSourceDirectory ?? '/non/existent/dir',
|
||||
resolvedProjectTargetDirectory,
|
||||
relativeWorkingDirectory
|
||||
? path.resolve(resolvedProjectTargetDirectory, relativeWorkingDirectory)
|
||||
: resolvedProjectTargetDirectory,
|
||||
'/non/existent/dir',
|
||||
staticContextContent ?? ({} as JobInterpolationContext)
|
||||
),
|
||||
skipCleanup ?? false
|
||||
const provider = new MockContextProvider(
|
||||
logger ?? createMockLogger(),
|
||||
runtimePlatform ?? BuildRuntimePlatform.LINUX,
|
||||
projectSourceDirectory ?? '/non/existent/dir',
|
||||
resolvedProjectTargetDirectory,
|
||||
relativeWorkingDirectory
|
||||
? path.resolve(resolvedProjectTargetDirectory, relativeWorkingDirectory)
|
||||
: resolvedProjectTargetDirectory,
|
||||
'/non/existent/dir',
|
||||
staticContextContent ?? ({} as JobInterpolationContext)
|
||||
);
|
||||
if (reportStepMetric) {
|
||||
provider.reportStepMetric = reportStepMetric;
|
||||
}
|
||||
return new BuildStepGlobalContext(provider, skipCleanup ?? false);
|
||||
}
|
||||
@@ -16,4 +16,4 @@ export * from './interpolation';
|
||||
export * from './utils/shell/spawn';
|
||||
export * from './utils/jsepEval';
|
||||
export * from './utils/hashFiles';
|
||||
export { StepMetric, StepMetricResult, StepMetricsCollection } from './StepMetrics';
|
||||
export { StepMetric, StepMetricResult } from './StepMetrics';
|
||||
@@ -350,10 +350,7 @@ describe('State sync mechanism', () => {
|
||||
it('should expose user-facing error from generic job', async () => {
|
||||
jest.mocked(runGenericJobAsync).mockResolvedValueOnce({
|
||||
runResult: result(
|
||||
new errors.UserFacingError(
|
||||
genericJobRunErrorCode,
|
||||
'ASC app was not found for this account.'
|
||||
)
|
||||
new errors.UserError(genericJobRunErrorCode, 'ASC app was not found for this account.')
|
||||
),
|
||||
buildWorkflow: {} as any,
|
||||
});
|
||||
|
||||
@@ -64,9 +64,7 @@ export async function build({
|
||||
case undefined: {
|
||||
artifacts = {};
|
||||
const buildCtx = ctx as BuildContext<Generic.Job>;
|
||||
const { runResult } = await runGenericJobAsync(buildCtx, {
|
||||
expoApiV2BaseUrl: config.wwwApiV2BaseUrl,
|
||||
});
|
||||
const { runResult } = await runGenericJobAsync(buildCtx);
|
||||
|
||||
if (!runResult.ok) {
|
||||
throw runResult.reason;
|
||||
|
||||
@@ -89,6 +89,7 @@ export function createBuildContext<TJob extends Job>({
|
||||
cacheManager: new GCSCacheManager(),
|
||||
metadata,
|
||||
reportBuildPhaseStats: reportBuildPhaseStatsFn,
|
||||
expoApiV2BaseUrl: config.wwwApiV2BaseUrl,
|
||||
});
|
||||
return ctx;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import config from './config';
|
||||
|
||||
class SystemDepsInstallError extends errors.UserFacingError {
|
||||
class SystemDepsInstallError extends errors.UserError {
|
||||
constructor(dependency: string) {
|
||||
super(
|
||||
'EAS_BUILD_SYSTEM_DEPS_INSTALL_ERROR',
|
||||
|
||||
@@ -374,7 +374,7 @@ function toBuildError(error: unknown, job: Job): errors.BuildError {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof errors.UserFacingError) {
|
||||
if (error instanceof errors.UserError) {
|
||||
return new errors.BuildError(error.message, {
|
||||
errorCode: error.errorCode,
|
||||
docsUrl: error.docsUrl,
|
||||
|
||||
Reference in new issue
Block a user