Compare commits

..
3 Commits
38 changed files with 366 additions and 977 deletions

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
with:
script: |
core.setFailed('Please add a changelog entry!')
- uses: mshick/add-pr-comment@v2.8.2
- uses: mshick/add-pr-comment@v2.1.0
if: always()
with:
message-id: changelog-entry-check
+2 -2
View File
@@ -8,8 +8,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Auto-detect non-interactive mode in CI and non-TTY environments. ([#3486](https://github.com/expo/eas-cli/pull/3486) by [@EvanBacon](https://github.com/EvanBacon))
- 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))
- 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
@@ -1676,6 +1675,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Add rollback disambiguation command. ([#2004](https://github.com/expo/eas-cli/pull/2004) by [@wschurman](https://github.com/wschurman))
- Detect devices that fail to be provisioned, list them to the user and show the explanation message with the link to the devices page to check actual status. ([#2011](https://github.com/expo/eas-cli/pull/2011) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
- Add info to EAS Update asset upload process about asset counts and limits. ([#2013](https://github.com/expo/eas-cli/pull/2013) by [@wschurman](https://github.com/wschurman))
- .nvmrc support for setting node version. ([#1954](https://github.com/expo/eas-cli/pull/1954) by [@khamilowicz](https://github.com/khamilowicz))
### 🐛 Bug fixes
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "18.4.0",
"version": "18.3.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/build-tools",
"version": "18.4.0",
"version": "18.2.0",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
"author": "Expo <[email protected]>",
@@ -38,14 +38,14 @@
"@expo/config": "10.0.6",
"@expo/config-plugins": "9.0.12",
"@expo/downloader": "18.0.1",
"@expo/eas-build-job": "18.4.0",
"@expo/eas-build-job": "18.2.0",
"@expo/env": "^0.4.0",
"@expo/logger": "18.0.1",
"@expo/package-manager": "1.9.10",
"@expo/plist": "^0.2.0",
"@expo/results": "^1.0.0",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "18.4.0",
"@expo/steps": "18.2.0",
"@expo/template-file": "18.0.1",
"@expo/turtle-spawn": "18.0.1",
"@expo/xcpretty": "^4.3.1",
@@ -67,26 +67,28 @@ export async function resolveBuildPhaseErrorAsync(
logLines: string[],
errorContext: ErrorContext,
buildLogsDirectory: string
): Promise<errors.ExpoError> {
): Promise<errors.BuildError> {
const { phase } = errorContext;
if (error instanceof errors.ExpoError) {
error.buildPhase ??= phase;
if (error instanceof errors.BuildError) {
return error;
}
const xcodeBuildLogs = await maybeReadXcodeBuildLogs(phase, buildLogsDirectory);
const userError =
resolveError(userErrorHandlers, logLines, errorContext, xcodeBuildLogs) ??
new errors.UnknownError(errorContext.phase);
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 !== userError.errorCode ? buildError.errorCode : undefined;
return new errors.UserError(userError.errorCode, userError.message, {
return new errors.BuildError(userError.message, {
errorCode: userError.errorCode,
trackingCode,
docsUrl: userError.docsUrl,
cause: error,
buildPhase: phase,
metadata: buildError?.metadata,
cause: error,
});
}
+1 -1
View File
@@ -287,7 +287,7 @@ export class BuildContext<TJob extends Job = Job> {
private async handleBuildPhaseErrorAsync(
err: any,
buildPhase: BuildPhase
): Promise<errors.ExpoError> {
): Promise<errors.BuildError> {
const buildError = await resolveBuildPhaseErrorAsync(
err,
this.logBuffer.getPhaseLogs(buildPhase),
@@ -177,34 +177,6 @@ describe(createInternalEasMaestroTestFunction, () => {
);
});
it('generates per-attempt report filenames when retrying', async () => {
// Mock sequence:
// 1. xcrun simctl shutdown → succeeds (beforeEach default is fine)
// 2. maestro test attempt 0 → fails (triggers retry)
// 3. maestro test attempt 1 → succeeds (beforeEach default)
mockedSpawnAsync
.mockResolvedValueOnce(undefined as any) // xcrun simctl shutdown
.mockRejectedValueOnce(new Error('Maestro test failed')); // maestro attempt 0
// Attempt 1 falls through to beforeEach's mockResolvedValue default
const step = createStep({
callInputs: { output_format: 'junit', retries: 2 },
});
await step.executeAsync();
// Filter to just the maestro test command calls (skip xcrun simctl shutdown)
const maestroCalls = mockedSpawnAsync.mock.calls.filter(([cmd]) => cmd === 'maestro');
expect(maestroCalls).toHaveLength(2);
// Extract --output paths from maestro calls
const outputArgs = maestroCalls.map(([, args]) => args[args.indexOf('--output') + 1]);
expect(outputArgs[0]).toContain('attempt-0');
expect(outputArgs[1]).toContain('attempt-1');
// Filenames must be unique (not overwriting)
expect(outputArgs[0]).not.toBe(outputArgs[1]);
});
it('fails Android flow when clone startup exhausts all attempts', async () => {
mockedAndroidUtils.startAsync
.mockResolvedValueOnce({
@@ -217,217 +217,6 @@ describe(parseMaestroResults, () => {
]);
});
it('returns per-attempt results when multiple JUnit files exist for the same flow', async () => {
vol.fromJSON({
// Attempt 0: login FAILED
'/junit/junit-report-flow-1-attempt-0.xml': [
'<?xml version="1.0"?>',
'<testsuites>',
' <testsuite name="Test Suite" tests="1" failures="1">',
' <testcase id="login" name="login" classname="login" time="5.0" status="ERROR">',
' <failure>Timeout</failure>',
' </testcase>',
' </testsuite>',
'</testsuites>',
].join('\n'),
// Attempt 1: login PASSED
'/junit/junit-report-flow-1-attempt-1.xml': [
'<?xml version="1.0"?>',
'<testsuites>',
' <testsuite name="Test Suite" tests="1" failures="0">',
' <testcase id="login" name="login" classname="login" time="3.0" status="SUCCESS"/>',
' </testsuite>',
'</testsuites>',
].join('\n'),
// ai-*.json metadata (2 timestamp dirs = 2 attempts)
'/tests/2026-01-28_055409/ai-login.json': JSON.stringify({
flow_name: 'login',
flow_file_path: '/root/project/.maestro/login.yml',
}),
'/tests/2026-01-28_055420/ai-login.json': JSON.stringify({
flow_name: 'login',
flow_file_path: '/root/project/.maestro/login.yml',
}),
});
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
// Should return 2 results — one per attempt
expect(results).toHaveLength(2);
expect(results).toEqual([
expect.objectContaining({
name: 'login',
path: '.maestro/login.yml',
status: 'failed',
errorMessage: 'Timeout',
duration: 5000,
retryCount: 0,
}),
expect.objectContaining({
name: 'login',
path: '.maestro/login.yml',
status: 'passed',
errorMessage: null,
duration: 3000,
retryCount: 1,
}),
]);
});
it('returns per-attempt results for reuse_devices=true (all flows in every attempt)', async () => {
vol.fromJSON({
// Attempt 0: home FAILED, login PASSED
'/junit-reports/android-maestro-junit-attempt-0.xml': [
'<?xml version="1.0"?>',
'<testsuites>',
' <testsuite name="Test Suite" tests="2" failures="1">',
' <testcase id="home" name="home" classname="home" time="5.0" status="ERROR">',
' <failure>Timeout</failure>',
' </testcase>',
' <testcase id="login" name="login" classname="login" time="3.0" status="SUCCESS"/>',
' </testsuite>',
'</testsuites>',
].join('\n'),
// Attempt 1: both PASSED
'/junit-reports/android-maestro-junit-attempt-1.xml': [
'<?xml version="1.0"?>',
'<testsuites>',
' <testsuite name="Test Suite" tests="2" failures="0">',
' <testcase id="home" name="home" classname="home" time="4.0" status="SUCCESS"/>',
' <testcase id="login" name="login" classname="login" time="2.0" status="SUCCESS"/>',
' </testsuite>',
'</testsuites>',
].join('\n'),
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
flow_name: 'home',
flow_file_path: '/root/project/.maestro/home.yml',
}),
'/tests/2026-01-28_055409/ai-login.json': JSON.stringify({
flow_name: 'login',
flow_file_path: '/root/project/.maestro/login.yml',
}),
'/tests/2026-01-28_055420/ai-home.json': JSON.stringify({
flow_name: 'home',
flow_file_path: '/root/project/.maestro/home.yml',
}),
'/tests/2026-01-28_055420/ai-login.json': JSON.stringify({
flow_name: 'login',
flow_file_path: '/root/project/.maestro/login.yml',
}),
});
const results = await parseMaestroResults('/junit-reports', '/tests', '/root/project');
// 4 results: 2 flows × 2 attempts
expect(results).toHaveLength(4);
expect(results).toEqual([
expect.objectContaining({ name: 'home', status: 'failed', retryCount: 0 }),
expect.objectContaining({ name: 'home', status: 'passed', retryCount: 1 }),
expect.objectContaining({ name: 'login', status: 'passed', retryCount: 0 }),
expect.objectContaining({ name: 'login', status: 'passed', retryCount: 1 }),
]);
});
it('returns per-attempt results with sharding (multiple testsuites per attempt file)', async () => {
vol.fromJSON({
// Attempt 0: shard 1 has home (FAILED), shard 2 has login (PASSED)
'/junit-reports/android-maestro-junit-attempt-0.xml': [
'<?xml version="1.0"?>',
'<testsuites>',
' <testsuite name="Test Suite" device="emulator-5554" tests="1" failures="1">',
' <testcase id="home" name="home" classname="home" time="5.0" status="ERROR">',
' <failure>Timeout</failure>',
' </testcase>',
' </testsuite>',
' <testsuite name="Test Suite" device="emulator-5556" tests="1" failures="0">',
' <testcase id="login" name="login" classname="login" time="3.0" status="SUCCESS"/>',
' </testsuite>',
'</testsuites>',
].join('\n'),
// Attempt 1: all passed across shards
'/junit-reports/android-maestro-junit-attempt-1.xml': [
'<?xml version="1.0"?>',
'<testsuites>',
' <testsuite name="Test Suite" device="emulator-5554" tests="1" failures="0">',
' <testcase id="home" name="home" classname="home" time="4.0" status="SUCCESS"/>',
' </testsuite>',
' <testsuite name="Test Suite" device="emulator-5556" tests="1" failures="0">',
' <testcase id="login" name="login" classname="login" time="2.0" status="SUCCESS"/>',
' </testsuite>',
'</testsuites>',
].join('\n'),
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
flow_name: 'home',
flow_file_path: '/root/project/.maestro/home.yml',
}),
'/tests/2026-01-28_055409/ai-login.json': JSON.stringify({
flow_name: 'login',
flow_file_path: '/root/project/.maestro/login.yml',
}),
'/tests/2026-01-28_055420/ai-home.json': JSON.stringify({
flow_name: 'home',
flow_file_path: '/root/project/.maestro/home.yml',
}),
'/tests/2026-01-28_055420/ai-login.json': JSON.stringify({
flow_name: 'login',
flow_file_path: '/root/project/.maestro/login.yml',
}),
});
const results = await parseMaestroResults('/junit-reports', '/tests', '/root/project');
expect(results).toHaveLength(4);
expect(results).toEqual([
expect.objectContaining({ name: 'home', status: 'failed', retryCount: 0 }),
expect.objectContaining({ name: 'home', status: 'passed', retryCount: 1 }),
expect.objectContaining({ name: 'login', status: 'passed', retryCount: 0 }),
expect.objectContaining({ name: 'login', status: 'passed', retryCount: 1 }),
]);
});
it('backward compat: reuse_devices=true with retries but old single JUnit file', async () => {
vol.fromJSON({
// Single overwritten JUnit (only has final attempt's results)
'/maestro-tests/android-maestro-junit.xml': [
'<?xml version="1.0"?>',
'<testsuites>',
' <testsuite name="Test Suite" tests="2" failures="0">',
' <testcase id="home" name="home" classname="home" time="4.0" status="SUCCESS"/>',
' <testcase id="login" name="login" classname="login" time="2.0" status="SUCCESS"/>',
' </testsuite>',
'</testsuites>',
].join('\n'),
// 2 timestamp dirs — both flows appear in both (entire suite retried)
'/maestro-tests/2026-01-28_055409/ai-home.json': JSON.stringify({
flow_name: 'home',
flow_file_path: '/root/project/.maestro/home.yml',
}),
'/maestro-tests/2026-01-28_055409/ai-login.json': JSON.stringify({
flow_name: 'login',
flow_file_path: '/root/project/.maestro/login.yml',
}),
'/maestro-tests/2026-01-28_055420/ai-home.json': JSON.stringify({
flow_name: 'home',
flow_file_path: '/root/project/.maestro/home.yml',
}),
'/maestro-tests/2026-01-28_055420/ai-login.json': JSON.stringify({
flow_name: 'login',
flow_file_path: '/root/project/.maestro/login.yml',
}),
});
const results = await parseMaestroResults('/maestro-tests', '/maestro-tests', '/root/project');
// Both flows have 2 occurrences → retryCount = 1 for both
expect(results).toHaveLength(2);
expect(results).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'home', status: 'passed', retryCount: 1 }),
expect.objectContaining({ name: 'login', status: 'passed', retryCount: 1 }),
])
);
});
it('handles reuse_devices=false (separate junit_report_directory)', async () => {
vol.fromJSON({
// JUnit in temp dir (per-flow files)
@@ -238,45 +238,6 @@ describe(createReportMaestroTestResultsFunction, () => {
expect(mockGraphqlClient.mutation).not.toHaveBeenCalled();
});
it('sends per-attempt results with same name but different retryCount', async () => {
vol.fromJSON({
// Two JUnit files for same flow (per-attempt)
'/junit/junit-report-flow-1-attempt-0.xml': [
'<?xml version="1.0"?>',
'<testsuites>',
' <testsuite name="Test Suite" tests="1" failures="1">',
' <testcase id="home" name="home" classname="home" time="5.0" status="ERROR">',
' <failure>Tap failed</failure>',
' </testcase>',
' </testsuite>',
'</testsuites>',
].join('\n'),
'/junit/junit-report-flow-1-attempt-1.xml': JUNIT_PASS,
'/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
'/tests/2026-01-28_055420/ai-home.json': FLOW_AI,
});
mockMutationFn.mockResolvedValue({
data: {
workflowDeviceTestCaseResult: {
createWorkflowDeviceTestCaseResults: [{ id: 'id-1' }, { id: 'id-2' }],
},
},
});
await createStep().executeAsync();
expect(mockGraphqlClient.mutation).toHaveBeenCalledTimes(1);
const [, variables] = (mockGraphqlClient.mutation as jest.Mock).mock.calls[0];
expect(variables.input.testCaseResults).toHaveLength(2);
expect(variables.input.testCaseResults[0]).toEqual(
expect.objectContaining({ name: 'home', status: 'FAILED', retryCount: 0 })
);
expect(variables.input.testCaseResults[1]).toEqual(
expect.objectContaining({ name: 'home', status: 'PASSED', retryCount: 1 })
);
});
it('uses default directories when inputs are not provided', async () => {
vol.fromJSON({
'/home/expo/.maestro/tests/report.xml': JUNIT_PASS,
@@ -214,17 +214,18 @@ export function createInternalEasMaestroTestFunction(ctx: CustomBuildContext): B
for (const [flowIndex, flowPath] of flowPathsToExecute.entries()) {
stepCtx.logger.info('');
// If output_format is empty or noop, we won't use this.
const outputPath = path.join(
maestroReportsDir,
[
`${output_format ? output_format + '-' : ''}report-flow-${flowIndex + 1}`,
MaestroOutputFormatToExtensionMap[output_format ?? 'noop'],
]
.filter(Boolean)
.join('.')
);
for (let attemptCount = 0; attemptCount < retries; attemptCount++) {
// Generate unique report path per attempt (not overwritten on retry)
const outputPath = path.join(
maestroReportsDir,
[
`${output_format ? output_format + '-' : ''}report-flow-${flowIndex + 1}-attempt-${attemptCount}`,
MaestroOutputFormatToExtensionMap[output_format ?? 'noop'],
]
.filter(Boolean)
.join('.')
);
const localDeviceName = `eas-simulator-${flowIndex}-${attemptCount}` as
| IosSimulatorName
| AndroidVirtualDeviceName;
@@ -275,11 +276,12 @@ export function createInternalEasMaestroTestFunction(ctx: CustomBuildContext): B
if (logsResult?.ok) {
try {
const extension = path.extname(logsResult.value.outputPath);
const destinationPath = path.join(
deviceLogsDir,
`flow-${flowIndex}-attempt-${attemptCount}${extension}`
);
const destinationPath = path.join(deviceLogsDir, `flow-${flowIndex}${extension}`);
await fs.promises.rm(destinationPath, {
force: true,
recursive: true,
});
await fs.promises.rename(logsResult.value.outputPath, destinationPath);
} catch (err) {
stepCtx.logger.warn({ err }, 'Failed to prepare device logs for upload.');
@@ -38,79 +38,6 @@ const xmlParser = new XMLParser({
isArray: name => ['testsuite', 'testcase', 'property'].includes(name),
});
// Internal helper — not exported. Parses a single JUnit XML file.
async function parseJUnitFile(filePath: string): Promise<JUnitTestCaseResult[]> {
const results: JUnitTestCaseResult[] = [];
try {
const content = await fs.readFile(filePath, 'utf-8');
const parsed = xmlParser.parse(content);
const testsuites = parsed?.testsuites?.testsuite;
if (!Array.isArray(testsuites)) {
return results;
}
for (const suite of testsuites) {
const testcases = suite?.testcase;
if (!Array.isArray(testcases)) {
continue;
}
for (const tc of testcases) {
const name = tc['@_name'];
if (!name) {
continue;
}
const timeStr = tc['@_time'];
const timeSeconds = timeStr ? parseFloat(timeStr) : 0;
const duration = Number.isFinite(timeSeconds) ? Math.round(timeSeconds * 1000) : 0;
const status: 'passed' | 'failed' = tc['@_status'] === 'SUCCESS' ? 'passed' : 'failed';
const failureText =
tc.failure != null
? typeof tc.failure === 'string'
? tc.failure
: (tc.failure?.['#text'] ?? null)
: null;
const errorText =
tc.error != null
? typeof tc.error === 'string'
? tc.error
: (tc.error?.['#text'] ?? null)
: null;
const errorMessage: string | null = failureText ?? errorText ?? null;
const rawProperties: { '@_name': string; '@_value': string }[] =
tc.properties?.property ?? [];
const properties: Record<string, string> = {};
for (const prop of rawProperties) {
const propName = prop['@_name'];
const value = prop['@_value'];
if (typeof propName !== 'string' || typeof value !== 'string') {
continue;
}
properties[propName] = value;
}
const tagsValue = properties['tags'];
const tags: string[] = tagsValue
? tagsValue
.split(',')
.map(t => t.trim())
.filter(Boolean)
: [];
delete properties['tags'];
results.push({ name, status, duration, errorMessage, tags, properties });
}
}
} catch {
// Skip malformed XML files
}
return results;
}
export async function parseJUnitTestCases(junitDirectory: string): Promise<JUnitTestCaseResult[]> {
let entries: string[];
try {
@@ -125,9 +52,83 @@ export async function parseJUnitTestCases(junitDirectory: string): Promise<JUnit
}
const results: JUnitTestCaseResult[] = [];
for (const xmlFile of xmlFiles) {
results.push(...(await parseJUnitFile(path.join(junitDirectory, xmlFile))));
try {
const content = await fs.readFile(path.join(junitDirectory, xmlFile), 'utf-8');
const parsed = xmlParser.parse(content);
const testsuites = parsed?.testsuites?.testsuite;
if (!Array.isArray(testsuites)) {
continue;
}
for (const suite of testsuites) {
const testcases = suite?.testcase;
if (!Array.isArray(testcases)) {
continue;
}
for (const tc of testcases) {
const name = tc['@_name'];
if (!name) {
continue;
}
const timeStr = tc['@_time'];
const timeSeconds = timeStr ? parseFloat(timeStr) : 0;
const duration = Number.isFinite(timeSeconds) ? Math.round(timeSeconds * 1000) : 0;
// Use @_status as primary indicator (more robust than checking <failure> presence)
const status: 'passed' | 'failed' = tc['@_status'] === 'SUCCESS' ? 'passed' : 'failed';
// Extract error message from <failure> or <error> elements
const failureText =
tc.failure != null
? typeof tc.failure === 'string'
? tc.failure
: (tc.failure?.['#text'] ?? null)
: null;
const errorText =
tc.error != null
? typeof tc.error === 'string'
? tc.error
: (tc.error?.['#text'] ?? null)
: null;
const errorMessage: string | null = failureText ?? errorText ?? null;
// Extract properties
const rawProperties: { '@_name': string; '@_value': string }[] =
tc.properties?.property ?? [];
const properties: Record<string, string> = {};
for (const prop of rawProperties) {
const propName = prop['@_name'];
const value = prop['@_value'];
if (typeof propName !== 'string' || typeof value !== 'string') {
continue;
}
properties[propName] = value;
}
// Extract tags from "tags" property (Maestro 2.2.0+, comma-separated)
const tagsValue = properties['tags'];
const tags: string[] = tagsValue
? tagsValue
.split(',')
.map(t => t.trim())
.filter(Boolean)
: [];
delete properties['tags'];
results.push({ name, status, duration, errorMessage, tags, properties });
}
}
} catch {
// Skip malformed XML files
continue;
}
}
return results;
}
@@ -166,32 +167,9 @@ export async function parseMaestroResults(
testsDirectory: string,
projectRoot: string
): Promise<MaestroFlowResult[]> {
// 1. Parse JUnit XML files, tracking which file each result came from
let junitEntries: string[];
try {
junitEntries = await fs.readdir(junitDirectory);
} catch {
return [];
}
const xmlFiles = junitEntries.filter(f => f.endsWith('.xml')).sort();
if (xmlFiles.length === 0) {
return [];
}
interface JUnitResultWithSource {
result: JUnitTestCaseResult;
sourceFile: string;
}
const junitResultsWithSource: JUnitResultWithSource[] = [];
for (const xmlFile of xmlFiles) {
const fileResults = await parseJUnitFile(path.join(junitDirectory, xmlFile));
for (const result of fileResults) {
junitResultsWithSource.push({ result, sourceFile: xmlFile });
}
}
if (junitResultsWithSource.length === 0) {
// 1. Parse JUnit XML files (primary source)
const junitResults = await parseJUnitTestCases(junitDirectory);
if (junitResults.length === 0) {
return [];
}
@@ -239,63 +217,25 @@ export async function parseMaestroResults(
// 3. Merge: JUnit results + ai-*.json metadata
const results: MaestroFlowResult[] = [];
// Parse attempt index from filename pattern: *-attempt-N.*
const ATTEMPT_PATTERN = /attempt-(\d+)/;
// Group results by flow name
const resultsByName = new Map<string, JUnitResultWithSource[]>();
for (const entry of junitResultsWithSource) {
const group = resultsByName.get(entry.result.name) ?? [];
group.push(entry);
resultsByName.set(entry.result.name, group);
}
for (const [flowName, flowEntries] of resultsByName) {
const flowFilePath = flowPathMap.get(flowName);
for (const junit of junitResults) {
const flowFilePath = flowPathMap.get(junit.name);
const relativePath = flowFilePath
? await relativizePathAsync(flowFilePath, projectRoot)
: flowName;
: junit.name; // fallback: use flow name if ai-*.json not found
if (flowEntries.length === 1) {
// Single result for this flow — use ai-*.json occurrence count for retryCount
// (backward compat with old-style single JUnit file that gets overwritten)
const { result } = flowEntries[0];
const occurrences = flowOccurrences.get(flowName) ?? 0;
const retryCount = Math.max(0, occurrences - 1);
const occurrences = flowOccurrences.get(junit.name) ?? 0;
const retryCount = Math.max(0, occurrences - 1);
results.push({
name: flowName,
path: relativePath,
status: result.status,
errorMessage: result.errorMessage,
duration: result.duration,
retryCount,
tags: result.tags,
properties: result.properties,
});
} else {
// Multiple results — per-attempt JUnit files. Sort by attempt index from filename.
const sorted = flowEntries
.map(entry => {
const match = entry.sourceFile.match(ATTEMPT_PATTERN);
const attemptIndex = match ? parseInt(match[1], 10) : 0;
return { ...entry, attemptIndex };
})
.sort((a, b) => a.attemptIndex - b.attemptIndex);
for (const { result, attemptIndex } of sorted) {
results.push({
name: flowName,
path: relativePath,
status: result.status,
errorMessage: result.errorMessage,
duration: result.duration,
retryCount: attemptIndex,
tags: result.tags,
properties: result.properties,
});
}
}
results.push({
name: junit.name,
path: relativePath,
status: junit.status,
errorMessage: junit.errorMessage,
duration: junit.duration,
retryCount,
tags: junit.tags,
properties: junit.properties,
});
}
return results;
@@ -64,21 +64,14 @@ export function createReportMaestroTestResultsFunction(ctx: CustomBuildContext):
return;
}
// Detect truly conflicting results: same (name, retryCount) pair means different flow files
// share the same name (Maestro config override), which we can't disambiguate.
// Same name with different retryCount is expected (per-attempt results from retries).
const seen = new Set<string>();
const conflicting = new Set<string>();
for (const r of flowResults) {
const key = `${r.name}:${r.retryCount}`;
if (seen.has(key)) {
conflicting.add(r.name);
}
seen.add(key);
}
if (conflicting.size > 0) {
// Maestro allows overriding flow names via config, so different flow files can share
// the same name. JUnit XML only contains names (not file paths), making it impossible
// to map duplicates back to their original flow files. Skip and let the user fix it.
const names = flowResults.map(r => r.name);
const duplicates = names.filter((n, i) => names.indexOf(n) !== i);
if (duplicates.length > 0) {
logger.error(
`Duplicate test case names found in JUnit output: ${[...conflicting].join(
`Duplicate test case names found in JUnit output: ${[...new Set(duplicates)].join(
', '
)}. Skipping report. Ensure each Maestro flow has a unique name.`
);
@@ -1,6 +1,6 @@
{
"name": "create-eas-build-function",
"version": "18.4.0",
"version": "18.2.0",
"description": "Create functions for use in EAS Build custom builds.",
"keywords": [
"expo",
@@ -35,7 +35,7 @@
"clean": "rimraf ./build/ \"*.tsbuildinfo\""
},
"dependencies": {
"@expo/steps": "18.4.0"
"@expo/steps": "18.2.0"
},
"devDependencies": {
"@expo/package-manager": "1.9.10",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/eas-build-job",
"version": "18.4.0",
"version": "18.2.0",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "MIT",
"author": "Expo <[email protected]>",
@@ -1,10 +1,8 @@
import { ErrorCode, ExpoError, SystemError, UserError } from '../errors';
import { BuildError, ExpoError, UserError } from '../errors';
import { BuildPhase } from '../logs';
type ExpoErrorDetails = ConstructorParameters<typeof ExpoError>[1];
class TestExpoError extends ExpoError {
constructor(message: string, details: ExpoErrorDetails) {
constructor(message: string, details: ConstructorParameters<typeof BuildError>[1]) {
super(message, details);
}
}
@@ -29,7 +27,27 @@ describe(ExpoError, () => {
});
expect(error.buildPhase).toBe(BuildPhase.PREBUILD);
expect(error.cause).toBe(cause);
expect(error.toExternalExpoError()).toEqual({
expect(error.format()).toEqual({
errorCode: 'ERR_CODE',
message: 'canonical message',
docsUrl: 'https://docs.example.dev',
buildPhase: BuildPhase.PREBUILD,
});
});
});
describe(BuildError, () => {
it('formats using canonical message and errorCode', () => {
const error = new BuildError('canonical message', {
errorCode: 'ERR_CODE',
docsUrl: 'https://docs.example.dev',
buildPhase: BuildPhase.PREBUILD,
metadata: { packageName: '@expo/config' },
});
expect(error).toBeInstanceOf(ExpoError);
expect(error.metadata).toEqual({ packageName: '@expo/config' });
expect(error.format()).toEqual({
errorCode: 'ERR_CODE',
message: 'canonical message',
docsUrl: 'https://docs.example.dev',
@@ -56,16 +74,6 @@ describe(UserError, () => {
expect(error.cause).toBe(cause);
});
it('supports trackingCode and buildPhase in options', () => {
const error = new UserError('ERR_CODE', 'message', {
trackingCode: 'TRACKING_CODE',
buildPhase: BuildPhase.PREBUILD,
});
expect(error.trackingCode).toBe('TRACKING_CODE');
expect(error.buildPhase).toBe(BuildPhase.PREBUILD);
});
it('supports docsUrl, metadata, and cause in options', () => {
const cause = new Error('root cause');
const error = new UserError('ERR_CODE', 'message', {
@@ -79,30 +87,3 @@ describe(UserError, () => {
expect(error.cause).toBe(cause);
});
});
describe(SystemError, () => {
it('always formats with SERVER_ERROR and preserves trackingCode', () => {
const cause = new Error('root cause');
const error = new SystemError('system message', {
trackingCode: 'TRACKING_CODE',
docsUrl: 'https://docs.example.dev',
buildPhase: BuildPhase.PREBUILD,
metadata: { packageName: '@expo/config' },
cause,
});
expect(error).toBeInstanceOf(ExpoError);
expect(error.errorCode).toBe(ErrorCode.SERVER_ERROR);
expect(error.trackingCode).toBe('TRACKING_CODE');
expect(error.docsUrl).toBe('https://docs.example.dev');
expect(error.metadata).toEqual({ packageName: '@expo/config' });
expect(error.buildPhase).toBe(BuildPhase.PREBUILD);
expect(error.cause).toBe(cause);
expect(error.toExternalExpoError()).toEqual({
errorCode: ErrorCode.SERVER_ERROR,
message: 'system message',
docsUrl: 'https://docs.example.dev',
buildPhase: BuildPhase.PREBUILD,
});
});
});
+19 -39
View File
@@ -54,10 +54,7 @@ export abstract class ExpoError<TMetadata extends ErrorMetadata = ErrorMetadata>
this.buildPhase = details.buildPhase;
}
/**
* Serialized error payload used by the orchestrator-worker API.
*/
public toExternalExpoError(): ExternalBuildError {
public format(): ExternalBuildError {
return {
errorCode: this.errorCode,
message: this.message,
@@ -67,49 +64,29 @@ export abstract class ExpoError<TMetadata extends ErrorMetadata = ErrorMetadata>
}
}
export class BuildError<
TMetadata extends ErrorMetadata = ErrorMetadata,
> extends ExpoError<TMetadata> {
constructor(message: string, details: ExpoErrorDetails<TMetadata>) {
super(message, details);
}
}
export class UserError<
TMetadata extends ErrorMetadata = ErrorMetadata,
> extends ExpoError<TMetadata> {
constructor(
public errorCode: string,
message: string,
public message: string,
options?: {
trackingCode?: string;
docsUrl?: string;
buildPhase?: BuildPhase;
metadata?: TMetadata;
cause?: unknown;
}
) {
super(message, {
errorCode,
trackingCode: options?.trackingCode,
docsUrl: options?.docsUrl,
buildPhase: options?.buildPhase,
metadata: options?.metadata,
cause: options?.cause,
});
}
}
export class SystemError<
TMetadata extends ErrorMetadata = ErrorMetadata,
> extends ExpoError<TMetadata> {
constructor(
message: string,
options?: {
trackingCode?: string;
docsUrl?: string;
buildPhase?: BuildPhase;
metadata?: TMetadata;
cause?: unknown;
}
) {
super(message, {
errorCode: ErrorCode.SERVER_ERROR,
trackingCode: options?.trackingCode,
docsUrl: options?.docsUrl,
buildPhase: options?.buildPhase,
metadata: options?.metadata,
cause: options?.cause,
});
@@ -122,25 +99,28 @@ export class UnknownError extends UserError {
ErrorCode.UNKNOWN_ERROR,
buildPhase
? `Unknown error. See logs of the ${buildPhaseDisplayName[buildPhase]} build phase for more information.`
: 'Unknown error. See logs for more information.',
{ buildPhase }
: 'Unknown error. See logs for more information.'
);
}
}
export class UnknownBuildError extends UserError {
export class UnknownBuildError extends BuildError {
constructor() {
const errorCode = ErrorCode.UNKNOWN_ERROR;
const message = 'Unknown error. See logs for more information.';
super(errorCode, message);
super(message, {
errorCode,
});
}
}
export class UnknownCustomBuildError extends UserError {
export class UnknownCustomBuildError extends BuildError {
constructor() {
const errorCode = ErrorCode.UNKNOWN_CUSTOM_BUILD_ERROR;
const message = 'Unknown custom build error. See logs for more information.';
super(errorCode, message);
super(message, {
errorCode,
});
}
}
+93 -94
View File
@@ -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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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`
@@ -488,20 +488,19 @@ run dev client simulator/emulator build with matching fingerprint or create a ne
```
USAGE
$ eas build:dev [-p ios|android] [-e PROFILE_NAME] [--skip-build-if-not-found]
$ eas build:dev [-p ios|android] [-e PROFILE_NAME]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. It must be a profile allowing to create
emulator/simulator internal distribution dev client builds. The "development-simulator"
build profile will be selected by default.
-p, --platform=<option> <options: ios|android>
--skip-build-if-not-found Skip build if no successful build with matching fingerprint is found.
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. It must be a profile allowing to create
emulator/simulator internal distribution dev client builds. The "development-simulator"
build profile will be selected by default.
-p, --platform=<option> <options: ios|android>
DESCRIPTION
run dev client simulator/emulator build with matching fingerprint or create a new one
```
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.4.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`
@@ -522,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.4.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`
@@ -560,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.4.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`
@@ -600,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.4.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`
@@ -630,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.4.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`
@@ -656,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.4.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`
@@ -708,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.4.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`
@@ -726,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.4.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`
@@ -744,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.4.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]`
@@ -761,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.4.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]`
@@ -782,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.4.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]`
@@ -803,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.4.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]`
@@ -825,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.4.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`
@@ -845,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.4.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]`
@@ -867,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.4.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]`
@@ -889,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.4.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]`
@@ -924,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.4.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]`
@@ -947,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.4.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`
@@ -968,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.4.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`
@@ -985,7 +984,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.4.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`
@@ -1003,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.4.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]`
@@ -1032,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.4.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`
@@ -1057,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.4.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]`
@@ -1078,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.4.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]`
@@ -1099,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.4.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`
@@ -1136,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.4.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`
@@ -1156,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.4.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`
@@ -1177,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.4.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`
@@ -1198,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.4.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]`
@@ -1212,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.4.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`
@@ -1226,7 +1225,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.4.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]`
@@ -1258,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.4.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]`
@@ -1284,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.4.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`
@@ -1306,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.4.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]`
@@ -1334,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.4.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]`
@@ -1362,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.4.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]`
@@ -1385,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.4.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]`
@@ -1407,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.4.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]`
@@ -1441,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.4.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]`
@@ -1485,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.4.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`
@@ -1516,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.4.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]`
@@ -1625,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.4.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`
@@ -1642,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.4.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`
@@ -1659,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.4.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]`
@@ -1711,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.4.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`
@@ -1725,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.4.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`
@@ -1748,7 +1747,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.4.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]`
@@ -1772,7 +1771,7 @@ ALIASES
$ eas new
```
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.4.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]`
@@ -1790,7 +1789,7 @@ ALIASES
$ eas onboarding
```
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.4.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`
@@ -1823,7 +1822,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.4.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`
@@ -1866,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.4.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`
@@ -1887,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.4.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`
@@ -1908,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.4.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]`
@@ -1933,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.4.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`
@@ -1960,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.4.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`
@@ -1998,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.4.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`
@@ -2026,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.4.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`
@@ -2056,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.4.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`
@@ -2077,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.4.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`
@@ -2097,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.4.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`
@@ -2118,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.4.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`
@@ -2140,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.4.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]`
@@ -2160,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.4.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`
@@ -2179,7 +2178,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.4.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`
@@ -2202,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.4.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`
@@ -2219,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.4.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`
@@ -2340,7 +2339,7 @@ DESCRIPTION
to cancel.
```
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.4.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]`
@@ -2360,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.4.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]`
@@ -2384,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.4.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`
@@ -2424,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.4.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`
@@ -2446,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.4.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]`
@@ -2474,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.4.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`
@@ -2494,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.4.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]`
@@ -2516,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.4.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 -->
-8
View File
@@ -1,12 +1,4 @@
/* eslint-disable no-console */
// Simulate an interactive environment so that commands default to interactive mode in tests.
// Without this, isNonInteractiveByDefault() returns true because:
// 1. Jest runs in a non-TTY environment (!process.stdin.isTTY)
// 2. CI environments set CI=1 (boolish('CI', false))
Object.defineProperty(process.stdin, 'isTTY', { value: true, writable: true });
delete process.env.CI;
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "eas-cli",
"version": "18.4.0",
"version": "18.3.0",
"description": "EAS command line tool",
"keywords": [
"cli",
@@ -47,8 +47,8 @@
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "10.0.6",
"@expo/config-plugins": "9.0.12",
"@expo/eas-build-job": "18.4.0",
"@expo/eas-json": "18.4.0",
"@expo/eas-build-job": "18.2.0",
"@expo/eas-json": "18.2.0",
"@expo/env": "^1.0.0",
"@expo/json-file": "8.3.3",
"@expo/logger": "18.0.1",
@@ -63,7 +63,7 @@
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "18.4.0",
"@expo/steps": "18.2.0",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^4.8.3",
"@oclif/plugin-autocomplete": "^3.2.40",
+8 -36
View File
@@ -455,9 +455,6 @@ const queueProgressBar = new cliProgress.SingleBar(
cliProgress.Presets.rect
);
let statusNewSetAt: number | null = null;
const NEW_STATUS_GRACE_PERIOD_MS = 15_000;
async function handleSingleBuildProgressAsync(
{
build,
@@ -489,19 +486,11 @@ async function handleSingleBuildProgressAsync(
case BuildStatus.Finished:
spinner.succeed('Build finished');
return { refetch: false };
case BuildStatus.New: {
const now = Date.now();
statusNewSetAt ??= now;
const newStatusDurationMs = now - statusNewSetAt;
if (newStatusDurationMs < NEW_STATUS_GRACE_PERIOD_MS) {
spinner.text = 'Waiting for build to get enqueued…';
} else {
spinner.text = `Build concurrency limit reached for your account. Build will enter queue once a concurrency becomes available. Add additional concurrencies at ${link(
formatAccountBillingUrl(accountName)
)}.`;
}
case BuildStatus.New:
spinner.text = `Build concurrency limit reached for your account. Build will enter queue once a concurrency becomes available. Add additional concurrencies at ${link(
formatAccountBillingUrl(accountName)
)}.`;
break;
}
case BuildStatus.InQueue: {
spinner.text = 'Build queued...';
const progressBarPayload =
@@ -564,7 +553,7 @@ const priorityToQueueDisplayName: Record<BuildPriority, string> = {
};
const statusToDisplayName: Record<BuildStatus, string> = {
[BuildStatus.New]: 'waiting to enter the queue',
[BuildStatus.New]: 'waiting to enter the queue (concurrency limit reached)',
[BuildStatus.InQueue]: 'in queue',
[BuildStatus.InProgress]: 'in progress',
[BuildStatus.PendingCancel]: 'canceled',
@@ -582,7 +571,6 @@ async function handleMultipleBuildsProgressAsync(
const buildCount = maybeBuilds.length;
const builds = maybeBuilds.filter<BuildFragment>(isBuildFragment);
const someNew = builds.some(build => build.status === BuildStatus.New);
const allFinished =
builds.filter(build => build.status === BuildStatus.Finished).length === buildCount;
const allSettled =
@@ -595,10 +583,6 @@ async function handleMultipleBuildsProgressAsync(
].includes(build.status)
).length === buildCount;
if (someNew && statusNewSetAt === null) {
statusNewSetAt = Date.now();
}
if (allSettled) {
if (allFinished) {
spinner.succeed(formatSettledBuildsText(builds));
@@ -607,11 +591,7 @@ async function handleMultipleBuildsProgressAsync(
}
return { refetch: false };
} else {
const showConcurrencyWarning =
someNew &&
statusNewSetAt !== null &&
Date.now() - statusNewSetAt >= NEW_STATUS_GRACE_PERIOD_MS;
spinner.text = formatPendingBuildsText(originalSpinnerText, builds, showConcurrencyWarning);
spinner.text = formatPendingBuildsText(originalSpinnerText, builds);
return { refetch: true };
}
}
@@ -630,20 +610,12 @@ function formatSettledBuildsText(builds: BuildFragment[]): string {
.join('\n ');
}
function formatPendingBuildsText(
originalSpinnerText: string,
builds: BuildFragment[],
showConcurrencyWarning: boolean
): string {
function formatPendingBuildsText(originalSpinnerText: string, builds: BuildFragment[]): string {
return [
originalSpinnerText,
...platforms.map(platform => {
const build = builds.find(build => build.platform === platform);
const status = build ? statusToDisplayName[build.status] : 'unknown';
const concurrencyWarning =
showConcurrencyWarning && build?.status === BuildStatus.New
? ' (concurrency limit reached)'
: '';
let extraInfo = '';
if (
build?.status === BuildStatus.InQueue &&
@@ -663,7 +635,7 @@ function formatPendingBuildsText(
}
return `${appPlatformEmojis[platform]} ${
appPlatformDisplayNames[platform]
} build - status: ${chalk.bold(status)}${concurrencyWarning}${extraInfo}`;
} build - status: ${chalk.bold(status)}${extraInfo}`;
}),
].join('\n ');
}
@@ -42,7 +42,7 @@ beforeEach(() => {
jest.mocked(createAnalyticsAsync).mockResolvedValue(analytics);
});
const createTestEasCommand = (): any => {
const createTestEasCommand = () => {
class TestEasCommand extends EasCommand {
override async runAsync(): Promise<void> {}
}
@@ -1,46 +1,4 @@
import { isNonInteractiveByDefault, resolveNonInteractiveAndJsonFlags } from '../flags';
describe(isNonInteractiveByDefault, () => {
const originalEnv = process.env;
const originalIsTTY = process.stdin.isTTY;
afterEach(() => {
process.env = originalEnv;
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, writable: true });
});
test('returns false when stdin is a TTY and CI is not set', () => {
process.env = { ...originalEnv };
delete process.env.CI;
Object.defineProperty(process.stdin, 'isTTY', { value: true, writable: true });
expect(isNonInteractiveByDefault()).toBe(false);
});
test('returns true when CI=1', () => {
process.env = { ...originalEnv, CI: '1' };
Object.defineProperty(process.stdin, 'isTTY', { value: true, writable: true });
expect(isNonInteractiveByDefault()).toBe(true);
});
test('returns true when CI=true', () => {
process.env = { ...originalEnv, CI: 'true' };
Object.defineProperty(process.stdin, 'isTTY', { value: true, writable: true });
expect(isNonInteractiveByDefault()).toBe(true);
});
test('returns true when stdin is not a TTY', () => {
process.env = { ...originalEnv };
delete process.env.CI;
Object.defineProperty(process.stdin, 'isTTY', { value: undefined, writable: true });
expect(isNonInteractiveByDefault()).toBe(true);
});
test('returns true when both CI is set and stdin is not a TTY', () => {
process.env = { ...originalEnv, CI: '1' };
Object.defineProperty(process.stdin, 'isTTY', { value: undefined, writable: true });
expect(isNonInteractiveByDefault()).toBe(true);
});
});
import { resolveNonInteractiveAndJsonFlags } from '../flags';
describe(resolveNonInteractiveAndJsonFlags, () => {
test('returns both false when no flags are set', () => {
@@ -1,9 +1,4 @@
import { Flags } from '@oclif/core';
import { boolish } from 'getenv';
export function isNonInteractiveByDefault(): boolean {
return boolish('CI', false) || !process.stdin.isTTY;
}
export const EasNonInteractiveAndJsonFlags = {
json: Flags.boolean({
@@ -12,8 +7,6 @@ export const EasNonInteractiveAndJsonFlags = {
}),
'non-interactive': Flags.boolean({
description: 'Run the command in non-interactive mode.',
default: () => Promise.resolve(isNonInteractiveByDefault()),
noCacheDefault: true,
}),
};
@@ -71,11 +64,6 @@ export const EASEnvironmentVariableScopeFlag = {
export const EASNonInteractiveFlag = {
'non-interactive': Flags.boolean({
description: 'Run the command in non-interactive mode.',
// eslint-disable-next-line async-protect/async-suffix
default: async () => {
return isNonInteractiveByDefault();
},
noCacheDefault: true,
}),
};
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/eas-json",
"version": "18.4.0",
"version": "18.2.0",
"description": "A library for interacting with eas.json",
"homepage": "https://github.com/expo/eas-cli",
"bugs": "https://github.com/expo/eas-cli/issues",
@@ -32,7 +32,7 @@
},
"dependencies": {
"@babel/code-frame": "7.23.5",
"@expo/eas-build-job": "18.4.0",
"@expo/eas-build-job": "18.2.0",
"chalk": "4.1.2",
"env-string": "1.0.1",
"fs-extra": "11.2.0",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "eas-cli-local-build-plugin",
"version": "18.4.0",
"version": "18.2.0",
"description": "Tool for running EAS compatible builds on a local machine.",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
@@ -26,8 +26,8 @@
"clean": "rm -rf node_modules dist coverage *.tsbuildinfo"
},
"dependencies": {
"@expo/build-tools": "18.4.0",
"@expo/eas-build-job": "18.4.0",
"@expo/build-tools": "18.2.0",
"@expo/eas-build-job": "18.2.0",
"@expo/logger": "18.0.1",
"@expo/spawn-async": "^1.7.2",
"@expo/turtle-spawn": "18.0.1",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/steps",
"version": "18.4.0",
"version": "18.2.0",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
"author": "Expo <[email protected]>",
@@ -27,7 +27,7 @@
"clean": "rm -rf node_modules dist coverage *.tsbuildinfo"
},
"dependencies": {
"@expo/eas-build-job": "18.4.0",
"@expo/eas-build-job": "18.2.0",
"@expo/logger": "18.0.1",
"@expo/spawn-async": "^1.7.2",
"arg": "^5.0.2",
-5
View File
@@ -3,11 +3,6 @@
"name": "EAS Worker",
"slug": "eas-worker",
"platforms": ["ios"],
"ios": {
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false
}
},
"extra": {
"eas": {
"projectId": "90798684-1384-4ce4-a73b-9f3fa3e3e368"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/worker",
"version": "18.4.0",
"version": "18.2.0",
"private": true,
"scripts": {
"start": "yarn watch",
@@ -19,16 +19,16 @@
},
"dependencies": {
"@babel/core": "7.24.7",
"@expo/build-tools": "18.4.0",
"@expo/build-tools": "18.2.0",
"@expo/config-plugins": "8.0.8",
"@expo/config-types": "52.0.1",
"@expo/downloader": "18.0.1",
"@expo/eas-build-job": "18.4.0",
"@expo/eas-build-job": "18.2.0",
"@expo/logger": "18.0.1",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "18.4.0",
"@expo/steps": "18.2.0",
"@expo/template-file": "18.0.1",
"@expo/turtle-spawn": "18.0.1",
"@hapi/boom": "10.0.1",
@@ -418,7 +418,8 @@ describe('State sync mechanism', () => {
it('should expose tracking code as internal error code from generic job', async () => {
jest.mocked(runGenericJobAsync).mockResolvedValueOnce({
runResult: result(
new errors.UserError(genericJobRunErrorCode, 'ASC app was not found for this account.', {
new errors.BuildError('ASC app was not found for this account.', {
errorCode: genericJobRunErrorCode,
trackingCode: 'GENERIC_TRACKING_ERROR',
})
),
@@ -484,75 +485,5 @@ describe('State sync mechanism', () => {
ws.close();
clearTimeout(messageTimeout);
});
it('should expose system errors with SERVER_ERROR and tracking code from generic job', async () => {
jest.mocked(runGenericJobAsync).mockResolvedValueOnce({
runResult: result(
new errors.SystemError('Internal service is unavailable.', {
trackingCode: 'GENERIC_SYSTEM_ERROR',
})
),
buildWorkflow: {} as any,
});
const dispatchWS = new WebSocket(`ws://localhost:${port}?expo_vm_name=${hostname()}`);
const dispatchHelper = new WsHelper(dispatchWS);
await dispatchHelper.onOpen();
dispatchWS.send(
JSON.stringify({
type: 'dispatch',
buildId,
job: createTestGenericJob(),
initiatingUserId: '14367e1b-26fc-4c00-aedb-0629d78f8286',
metadata: {
trackingContext: {},
},
})
);
dispatchWS.close();
await dispatchHelper.onClose();
const ws = new WebSocket(`ws://localhost:${port}?expo_vm_name=${hostname()}`);
const helper = new WsHelper(ws);
let stateResponsePromiseResolve: () => void;
const stateResponsePromise = new Promise<void>(res => {
stateResponsePromiseResolve = res;
});
const onMessage = jest.fn((message: any) => {
clearTimeout(messageTimeout);
try {
expect(message).toBeTruthy();
expect(message.type).toBe('state-response');
expect(message.status).toBe('error');
expect(message.externalBuildError).toEqual({
errorCode: errors.ErrorCode.SERVER_ERROR,
message: 'Internal service is unavailable.',
});
expect(message.internalErrorCode).toBe('GENERIC_SYSTEM_ERROR');
} catch (err) {
throw err;
} finally {
stateResponsePromiseResolve();
}
});
const openPromise = helper.onOpen();
helper.onMessage(onMessage);
await openPromise;
messageTimeout = setTimeout(() => {
unreachableCode('state-response timeout');
}, 3000);
ws.send(JSON.stringify({ type: 'state-query', buildId }));
await stateResponsePromise;
expect(helper.onErrorCb).not.toHaveBeenCalled();
expect(helper.onOpenCb).toHaveBeenCalled();
expect(helper.onMessageCb).toHaveBeenCalled();
expect(helper.onCloseCb).not.toHaveBeenCalled();
ws.close();
clearTimeout(messageTimeout);
});
});
});
@@ -4,47 +4,6 @@ import { Response } from 'node-fetch';
import fetch from 'node-fetch';
import z from 'zod';
jest.mock('@expo/build-tools', () => {
const actual = jest.requireActual('@expo/build-tools');
const { Writable } = jest.requireActual('node:stream');
class MockGCSLoggerStream extends Writable {
public static readonly CompressionMethod = actual.GCSLoggerStream.CompressionMethod;
public static readonly instances: MockGCSLoggerStream[] = [];
public readonly writes: any[] = [];
constructor(...args: any[]) {
super({ objectMode: true });
void args;
MockGCSLoggerStream.instances.push(this);
}
public async init(): Promise<string> {
return 'https://gcs.expo.test/logs';
}
public async cleanUp(): Promise<void> {}
public _write(
chunk: unknown,
_encoding: BufferEncoding,
callback: (error?: Error | null) => void
): void {
this.writes.push(chunk);
callback(null);
}
}
return {
...actual,
GCS: {
uploadWithSignedUrl: jest.fn(),
},
GCSLoggerStream: MockGCSLoggerStream,
};
});
import config from '../config';
import { createBuildLoggerWithSecretsFilter } from '../logger';
@@ -65,29 +24,19 @@ async function waitForStreamFlush(): Promise<void> {
}
const fetchMock = jest.mocked(fetch);
const { GCSLoggerStream } = jest.requireMock('@expo/build-tools') as {
GCSLoggerStream: {
instances: Array<{
writes: any[];
}>;
};
};
describe('logger', () => {
const originalHttpBaseUrl = config.loggers.http.baseUrl;
const originalBuildId = config.buildId;
const originalGcsSignedUploadUrlForLogs = config.loggers.gcs.signedUploadUrlForLogs;
beforeEach(() => {
fetchMock.mockReset();
fetchMock.mockResolvedValue(new Response('', { status: 200, statusText: 'OK' }));
GCSLoggerStream.instances.length = 0;
});
afterEach(() => {
config.loggers.http.baseUrl = originalHttpBaseUrl;
config.buildId = originalBuildId;
config.loggers.gcs.signedUploadUrlForLogs = originalGcsSignedUploadUrlForLogs;
});
it('obfuscates secrets in logs', async () => {
@@ -278,38 +227,6 @@ describe('logger', () => {
expect(secondAttemptOriginalLog).toEqual(firstAttemptOriginalLog);
});
it('does not forward HTTP upload failures to the GCS build log stream', async () => {
config.loggers.http.baseUrl = 'https://logs.expo.test/logs/';
config.loggers.gcs.signedUploadUrlForLogs = {
url: 'https://gcs.expo.test/logs',
headers: {},
};
config.buildId = 'build-id';
fetchMock.mockResolvedValue(
new Response('upload failed', { status: 500, statusText: 'Internal Server Error' })
);
const { logger, cleanUp } = await createBuildLoggerWithSecretsFilter({
robotAccessToken: 'robot-token',
});
logger.info('Actual build log');
await cleanUp();
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(GCSLoggerStream.instances).toHaveLength(1);
const gcsLogs = GCSLoggerStream.instances[0].writes;
expect(gcsLogs).toEqual(
expect.arrayContaining([expect.objectContaining({ msg: 'Actual build log' })])
);
expect(gcsLogs).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ msg: 'Failed to send logs batch over HTTP' }),
])
);
});
it('drains transformed logs even without explicit output consumer', async () => {
const { logger, outputStream } = await createBuildLoggerWithSecretsFilter({});
+1 -1
View File
@@ -101,7 +101,7 @@ function logBuildError(logger: bunyan, analytics: Analytics, err: Error): void {
const l = logger.child({ phase: BuildPhase.FAIL_BUILD });
l.info({ marker: LogMarker.START_PHASE }, `Start phase: ${BuildPhase.FAIL_BUILD}`);
if (err instanceof errors.ExpoError) {
if (err instanceof errors.BuildError) {
const internalErrorCode = err.trackingCode ?? err.errorCode;
analytics.logEvent(Event.WORKER_BUILD_FAIL, {
reason: err?.message,
+1 -6
View File
@@ -45,12 +45,7 @@ export default {
}),
},
http: {
baseUrl:
process.env.ENVIRONMENT === 'development'
? 'http://localhost:4999/logs/'
: process.env.ENVIRONMENT === 'staging'
? 'https://staging-logs.expo.dev/logs/'
: null,
baseUrl: process.env.ENVIRONMENT === 'development' ? 'http://localhost:4999/logs/' : null,
},
},
buildCache: {
+9 -1
View File
@@ -1,6 +1,7 @@
import { bunyan } from '@expo/logger';
import * as sentry from '@sentry/node';
import Flatted from 'flatted';
import { ZodError } from 'zod';
import { boomify } from '../utils/boom';
@@ -33,13 +34,20 @@ class Sentry {
const l = options.logger ?? this.logger;
const tags = { ...this.options.tags, ...options.tags };
const extras = options.extras ?? {};
// TurtleErrors don't need the explanatory ${msg} prefix,
// because their message should already be user-facing.
// ZodError#message can't be modified, so we don't reassign either.
if (err instanceof Error && !(err instanceof ZodError)) {
err.message = `${msg}\n${err.message}`;
}
const boom = err instanceof Error ? boomify(err) : err;
l.error({ tags, extras, err: boom }, msg);
sentry.withScope((scope: sentry.Scope) => {
scope.setTags(tags);
scope.setExtras(extras);
scope.setExtra('message', msg);
if (options?.level) {
scope.setLevel(options?.level);
}
+1 -1
View File
@@ -152,7 +152,7 @@ export async function createBuildLoggerWithSecretsFilter(secrets: Job['secrets']
headers: {
Authorization: `Bearer ${secrets.robotAccessToken}`,
},
logger: defaultLogger,
logger: buildLogger,
bufferRetentionMs: config.loggers.gcs.signedUploadUrlForLogs
? Math.max(30_000, config.loggers.gcs.uploadIntervalMs * 2)
: null,
+30 -16
View File
@@ -109,13 +109,13 @@ export default class BuildService {
this.startBuildInternal({ job, metadata, initiatingUserId, projectId });
}
public async finishError(err: errors.ExpoError, artifacts: Artifacts | null): Promise<void> {
public async finishError(err: errors.BuildError, artifacts: Artifacts | null): Promise<void> {
logger.error({ err }, 'Job finished with error');
this.state.finish(Worker.Status.ERROR, {
applicationArchiveName: artifacts?.APPLICATION_ARCHIVE ?? null,
buildArtifactsName: artifacts?.BUILD_ARTIFACTS ?? null,
expoError: err,
userError: err,
});
const isSocketClosed: boolean = !this.ws;
// wait 5 seconds to make sure all logs are flushed
@@ -127,7 +127,7 @@ export default class BuildService {
type: WorkerMessage.MessageType.ERROR,
applicationArchiveName: artifacts?.APPLICATION_ARCHIVE ?? null,
buildArtifactsName: artifacts?.BUILD_ARTIFACTS ?? null,
externalBuildError: err.toExternalExpoError(),
externalBuildError: err.format(),
internalErrorCode: err.trackingCode ?? err.errorCode,
});
}
@@ -308,36 +308,31 @@ export default class BuildService {
await this.finishSuccess(artifacts);
} catch (error: any) {
const maybeArtifacts = (error.artifacts as Artifacts | undefined) ?? null;
const err =
error instanceof errors.ExpoError
? error
: 'mode' in job && [BuildMode.CUSTOM, BuildMode.REPACK].includes(job.mode)
? new errors.UnknownCustomBuildError()
: new errors.UnknownBuildError();
const maybeRawError =
error instanceof errors.ExpoError && error.cause instanceof Error ? error.cause : error;
const err = toBuildError(error, job);
const maybeRawError = error instanceof errors.BuildError ? (error.cause ?? error) : error;
const internalErrorCode = err.trackingCode ?? err.errorCode;
sentry.handleError(err.message, maybeRawError, {
tags: {
...(err.buildPhase ? { buildPhase: err.buildPhase } : {}),
errorCode: err.trackingCode ?? err.errorCode,
errorCode: internalErrorCode,
...('type' in job ? { workflow: job.type } : {}),
},
extras: {
buildId: this.buildId,
...(err.metadata ? { errorMetadata: err.metadata } : {}),
...(maybeRawError?.stdout ? { stdout: getLastNLines(100, maybeRawError.stdout) } : {}),
...(maybeRawError?.stderr ? { stderr: getLastNLines(100, maybeRawError.stderr) } : {}),
...(maybeRawError.stdout ? { stdout: getLastNLines(100, maybeRawError.stdout) } : {}),
...(maybeRawError.stderr ? { stderr: getLastNLines(100, maybeRawError.stderr) } : {}),
},
});
const robotAccessToken = job.secrets?.robotAccessToken;
if (robotAccessToken && err.errorCode === errors.ErrorCode.UNKNOWN_ERROR) {
let rawErrorMessage: string = '';
if (maybeRawError?.stderr) {
if (maybeRawError.stderr) {
rawErrorMessage += '\n' + getLastNLines(100, maybeRawError.stderr);
}
if (maybeRawError?.stdout) {
if (maybeRawError.stdout) {
rawErrorMessage += '\n' + getLastNLines(100, maybeRawError.stdout);
}
@@ -374,6 +369,25 @@ export default class BuildService {
}
}
function toBuildError(error: unknown, job: Job): errors.BuildError {
if (error instanceof errors.BuildError) {
return error;
}
if (error instanceof errors.UserError) {
return new errors.BuildError(error.message, {
errorCode: error.errorCode,
docsUrl: error.docsUrl,
cause: error.cause ?? error,
metadata: error.metadata,
});
}
return 'mode' in job && [BuildMode.CUSTOM, BuildMode.REPACK].includes(job.mode)
? new errors.UnknownCustomBuildError()
: new errors.UnknownBuildError();
}
function getLastNLines(numberOfLines: number, stream: string): string {
const lines = stream.split('\n');
if (lines.length <= numberOfLines) {
+7 -7
View File
@@ -8,7 +8,7 @@ class State {
private status: Worker.Status = Worker.Status.NEW;
private applicationArchiveName: string | null = null;
private buildArtifactsName: string | null = null;
private expoError?: errors.ExpoError;
private userError?: errors.BuildError;
private abortReason?: LauncherMessage.AbortReason;
private readonly shouldCloseWorker = false;
@@ -25,17 +25,17 @@ class State {
{
applicationArchiveName,
buildArtifactsName,
expoError,
userError,
}: {
applicationArchiveName: string | null;
buildArtifactsName: string | null;
expoError?: errors.ExpoError;
userError?: errors.BuildError;
}
): void {
this.status = result;
this.applicationArchiveName = applicationArchiveName;
this.buildArtifactsName = buildArtifactsName;
this.expoError = expoError;
this.userError = userError;
}
public setAbortReason(reason: LauncherMessage.AbortReason): void {
@@ -48,9 +48,9 @@ class State {
status: this.status,
buildArtifactsName: this.buildArtifactsName ?? null,
applicationArchiveName: this.applicationArchiveName ?? null,
...(this.expoError && {
externalBuildError: this.expoError.toExternalExpoError(),
internalErrorCode: this.expoError.trackingCode ?? this.expoError.errorCode,
...(this.userError && {
externalBuildError: this.userError.format(),
internalErrorCode: this.userError.trackingCode ?? this.userError.errorCode,
}),
abortReason: this.abortReason,
};
+2 -2
View File
@@ -21,7 +21,7 @@ export default class HttpLogStream extends Writable {
private readonly buffer: BufferedLogEntry[] = [];
private readonly url: string;
private readonly headers: Record<string, string>;
private readonly logger: bunyan;
private readonly logger?: bunyan;
private readonly maxBatchBytes: number;
private readonly bufferRetentionMs: number | null;
private inFlightRequest: Promise<void> | null = null;
@@ -98,7 +98,7 @@ export default class HttpLogStream extends Writable {
if (!isCleanup) {
// Keep logs in memory if upload fails so a later flush can retry.
this.buffer.unshift(...batch);
this.logger.error({ err }, 'Failed to send logs batch over HTTP');
this.logger?.error({ err }, 'Failed to send logs batch over HTTP');
}
})
.finally(() => {
Generated
+17 -17
View File
@@ -2700,14 +2700,14 @@ __metadata:
languageName: node
linkType: hard
"@expo/build-tools@npm:18.4.0, @expo/build-tools@workspace:packages/build-tools":
"@expo/build-tools@npm:18.2.0, @expo/build-tools@workspace:packages/build-tools":
version: 0.0.0-use.local
resolution: "@expo/build-tools@workspace:packages/build-tools"
dependencies:
"@expo/config": "npm:10.0.6"
"@expo/config-plugins": "npm:9.0.12"
"@expo/downloader": "npm:18.0.1"
"@expo/eas-build-job": "npm:18.4.0"
"@expo/eas-build-job": "npm:18.2.0"
"@expo/env": "npm:^0.4.0"
"@expo/logger": "npm:18.0.1"
"@expo/package-manager": "npm:1.9.10"
@@ -2715,7 +2715,7 @@ __metadata:
"@expo/repack-app": "npm:~0.2.5"
"@expo/results": "npm:^1.0.0"
"@expo/spawn-async": "npm:1.7.2"
"@expo/steps": "npm:18.4.0"
"@expo/steps": "npm:18.2.0"
"@expo/template-file": "npm:18.0.1"
"@expo/turtle-spawn": "npm:18.0.1"
"@expo/xcpretty": "npm:^4.3.1"
@@ -2986,7 +2986,7 @@ __metadata:
languageName: unknown
linkType: soft
"@expo/eas-build-job@npm:18.4.0, @expo/eas-build-job@workspace:packages/eas-build-job":
"@expo/eas-build-job@npm:18.2.0, @expo/eas-build-job@workspace:packages/eas-build-job":
version: 0.0.0-use.local
resolution: "@expo/eas-build-job@workspace:packages/eas-build-job"
dependencies:
@@ -3002,12 +3002,12 @@ __metadata:
languageName: unknown
linkType: soft
"@expo/eas-json@npm:18.4.0, @expo/eas-json@workspace:packages/eas-json":
"@expo/eas-json@npm:18.2.0, @expo/eas-json@workspace:packages/eas-json":
version: 0.0.0-use.local
resolution: "@expo/eas-json@workspace:packages/eas-json"
dependencies:
"@babel/code-frame": "npm:7.23.5"
"@expo/eas-build-job": "npm:18.4.0"
"@expo/eas-build-job": "npm:18.2.0"
"@tsconfig/node20": "npm:20.1.8"
"@types/babel__code-frame": "npm:7.0.3"
"@types/fs-extra": "npm:11.0.4"
@@ -3328,11 +3328,11 @@ __metadata:
languageName: node
linkType: hard
"@expo/steps@npm:18.4.0, @expo/steps@workspace:packages/steps":
"@expo/steps@npm:18.2.0, @expo/steps@workspace:packages/steps":
version: 0.0.0-use.local
resolution: "@expo/steps@workspace:packages/steps"
dependencies:
"@expo/eas-build-job": "npm:18.4.0"
"@expo/eas-build-job": "npm:18.2.0"
"@expo/logger": "npm:18.0.1"
"@expo/spawn-async": "npm:^1.7.2"
"@jest/globals": "npm:^29.7.0"
@@ -3398,16 +3398,16 @@ __metadata:
resolution: "@expo/worker@workspace:packages/worker"
dependencies:
"@babel/core": "npm:7.24.7"
"@expo/build-tools": "npm:18.4.0"
"@expo/build-tools": "npm:18.2.0"
"@expo/config-plugins": "npm:8.0.8"
"@expo/config-types": "npm:52.0.1"
"@expo/downloader": "npm:18.0.1"
"@expo/eas-build-job": "npm:18.4.0"
"@expo/eas-build-job": "npm:18.2.0"
"@expo/logger": "npm:18.0.1"
"@expo/results": "npm:1.0.0"
"@expo/rudder-sdk-node": "npm:1.1.1"
"@expo/spawn-async": "npm:1.7.2"
"@expo/steps": "npm:18.4.0"
"@expo/steps": "npm:18.2.0"
"@expo/template-file": "npm:18.0.1"
"@expo/turtle-spawn": "npm:18.0.1"
"@hapi/boom": "npm:10.0.1"
@@ -10164,7 +10164,7 @@ __metadata:
resolution: "create-eas-build-function@workspace:packages/create-eas-build-function"
dependencies:
"@expo/package-manager": "npm:1.9.10"
"@expo/steps": "npm:18.4.0"
"@expo/steps": "npm:18.2.0"
"@jest/globals": "npm:^29.7.0"
"@types/jest": "npm:^29.5.12"
"@types/node": "npm:20.14.2"
@@ -10777,8 +10777,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "eas-cli-local-build-plugin@workspace:packages/local-build-plugin"
dependencies:
"@expo/build-tools": "npm:18.4.0"
"@expo/eas-build-job": "npm:18.4.0"
"@expo/build-tools": "npm:18.2.0"
"@expo/eas-build-job": "npm:18.2.0"
"@expo/logger": "npm:18.0.1"
"@expo/spawn-async": "npm:^1.7.2"
"@expo/turtle-spawn": "npm:18.0.1"
@@ -10833,8 +10833,8 @@ __metadata:
"@expo/code-signing-certificates": "npm:0.0.5"
"@expo/config": "npm:10.0.6"
"@expo/config-plugins": "npm:9.0.12"
"@expo/eas-build-job": "npm:18.4.0"
"@expo/eas-json": "npm:18.4.0"
"@expo/eas-build-job": "npm:18.2.0"
"@expo/eas-json": "npm:18.2.0"
"@expo/env": "npm:^1.0.0"
"@expo/json-file": "npm:8.3.3"
"@expo/logger": "npm:18.0.1"
@@ -10849,7 +10849,7 @@ __metadata:
"@expo/results": "npm:1.0.0"
"@expo/rudder-sdk-node": "npm:1.1.1"
"@expo/spawn-async": "npm:1.7.2"
"@expo/steps": "npm:18.4.0"
"@expo/steps": "npm:18.2.0"
"@expo/timeago.js": "npm:1.0.0"
"@graphql-codegen/cli": "npm:5.0.0"
"@graphql-codegen/introspection": "npm:4.0.0"