mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df8ebc8f84 | ||
|
|
62ab329f39 | ||
|
|
198b796804 | ||
|
|
110055d943 | ||
|
|
06e2ba5bdb | ||
|
|
15690e7f6b | ||
|
|
134e74f50b | ||
|
|
4b3dcda99d | ||
|
|
0c32aa3a39 | ||
|
|
59ac8ae77a | ||
|
|
3e13df470e | ||
|
|
83877f6285 | ||
|
|
56018338e7 | ||
|
|
0fdf2dd44a | ||
|
|
0dcd6d465a | ||
|
|
d5da4f4167 | ||
|
|
d0b9281bb2 |
No files matched your search
+18
-2
@@ -8,12 +8,28 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use authorization code flow with PKCE for browser-based login. ([#3398](https://github.com/expo/eas-cli/pull/3398) by [@byronkarlen](https://github.com/byronkarlen))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [18.0.5](https://github.com/expo/eas-cli/releases/tag/v18.0.5) - 2026-02-25
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Require --environment flag for update commands when SDK >= 55. ([#3398](https://github.com/expo/eas-cli/pull/3398) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [18.0.4](https://github.com/expo/eas-cli/releases/tag/v18.0.4) - 2026-02-23
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Revert authorization code flow with PKCE for browser-based login. ([#3422](https://github.com/expo/eas-cli/pull/3422) by [@byronkarlen](https://github.com/byronkarlen))
|
||||
|
||||
## [18.0.3](https://github.com/expo/eas-cli/releases/tag/v18.0.3) - 2026-02-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use authorization code flow with PKCE for browser-based login. ([#3398](https://github.com/expo/eas-cli/pull/3398) by [@byronkarlen](https://github.com/byronkarlen))
|
||||
|
||||
## [18.0.1](https://github.com/expo/eas-cli/releases/tag/v18.0.1) - 2026-02-12
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "18.0.2",
|
||||
"version": "18.0.6",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/build-tools",
|
||||
"version": "18.0.2",
|
||||
"version": "18.0.6",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -53,6 +53,7 @@
|
||||
"@urql/core": "^6.0.1",
|
||||
"bplist-parser": "0.3.2",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fast-xml-parser": "^4.4.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"gql.tada": "^1.8.13",
|
||||
"joi": "^17.13.1",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { BuildRuntimePlatform, ExternalBuildContextProvider } from '@expo/steps';
|
||||
import { Client } from '@urql/core';
|
||||
import assert from 'assert';
|
||||
import path from 'path';
|
||||
|
||||
@@ -53,6 +54,7 @@ export class CustomBuildContext<TJob extends Job = Job> implements ExternalBuild
|
||||
public readonly startTime: Date;
|
||||
|
||||
public readonly logger: bunyan;
|
||||
public readonly graphqlClient: Client;
|
||||
public readonly runtimeApi: BuilderRuntimeApi;
|
||||
public job: TJob;
|
||||
public metadata?: Metadata;
|
||||
@@ -65,6 +67,7 @@ export class CustomBuildContext<TJob extends Job = Job> implements ExternalBuild
|
||||
this.metadata = buildCtx.metadata;
|
||||
|
||||
this.logger = buildCtx.logger.child({ phase: BuildPhase.CUSTOM });
|
||||
this.graphqlClient = buildCtx.graphqlClient;
|
||||
this.projectSourceDirectory = path.join(buildCtx.workingdir, 'temporary-custom-build');
|
||||
this.projectTargetDirectory = path.join(buildCtx.workingdir, 'build');
|
||||
this.defaultWorkingDirectory = buildCtx.getReactNativeProjectDirectory();
|
||||
|
||||
@@ -21,6 +21,7 @@ import { createInternalEasMaestroTestFunction } from './functions/internalMaestr
|
||||
import { createPrebuildBuildFunction } from './functions/prebuild';
|
||||
import { createReadIpaInfoBuildFunction } from './functions/readIpaInfo';
|
||||
import { createRepackBuildFunction } from './functions/repack';
|
||||
import { createReportMaestroTestResultsFunction } from './functions/reportMaestroTestResults';
|
||||
import { resolveAppleTeamIdFromCredentialsFunction } from './functions/resolveAppleTeamIdFromCredentials';
|
||||
import { createResolveBuildConfigBuildFunction } from './functions/resolveBuildConfig';
|
||||
import {
|
||||
@@ -81,6 +82,8 @@ export function getEasFunctions(ctx: CustomBuildContext): BuildFunction[] {
|
||||
createUploadToAscBuildFunction(),
|
||||
|
||||
createInternalEasMaestroTestFunction(ctx),
|
||||
|
||||
createReportMaestroTestResultsFunction(ctx),
|
||||
];
|
||||
|
||||
if (ctx.hasBuildJob()) {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { spawnAsync } from '@expo/steps';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { createGlobalContextMock } from '../../../__tests__/utils/context';
|
||||
import { createMockLogger } from '../../../__tests__/utils/logger';
|
||||
import { IosSimulatorUtils } from '../../../utils/IosSimulatorUtils';
|
||||
import { findMaestroPathsFlowsToExecuteAsync } from '../../../utils/findMaestroPathsFlowsToExecuteAsync';
|
||||
import { createInternalEasMaestroTestFunction } from '../internalMaestroTest';
|
||||
|
||||
jest.mock('@expo/steps', () => ({
|
||||
...jest.requireActual('@expo/steps'),
|
||||
spawnAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/IosSimulatorUtils');
|
||||
jest.mock('../../../utils/AndroidEmulatorUtils');
|
||||
jest.mock('../../../utils/findMaestroPathsFlowsToExecuteAsync');
|
||||
|
||||
const mockedSpawnAsync = jest.mocked(spawnAsync);
|
||||
const mockedIosUtils = jest.mocked(IosSimulatorUtils);
|
||||
const mockedFindFlows = jest.mocked(findMaestroPathsFlowsToExecuteAsync);
|
||||
|
||||
describe(createInternalEasMaestroTestFunction, () => {
|
||||
const mockUploadArtifact = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vol.mkdirSync(os.tmpdir(), { recursive: true });
|
||||
|
||||
mockedSpawnAsync.mockResolvedValue(undefined as any);
|
||||
mockUploadArtifact.mockResolvedValue({ artifactId: null });
|
||||
|
||||
mockedIosUtils.getAvailableDevicesAsync.mockResolvedValue([
|
||||
{ name: 'iPhone 15', udid: 'test-udid-123' } as any,
|
||||
]);
|
||||
mockedIosUtils.cloneAsync.mockResolvedValue(undefined as any);
|
||||
mockedIosUtils.startAsync.mockResolvedValue({ udid: 'cloned-udid' } as any);
|
||||
mockedIosUtils.waitForReadyAsync.mockResolvedValue(undefined as any);
|
||||
mockedIosUtils.collectLogsAsync.mockResolvedValue({ outputPath: '/tmp/logs.txt' } as any);
|
||||
mockedIosUtils.deleteAsync.mockResolvedValue(undefined as any);
|
||||
|
||||
mockedFindFlows.mockResolvedValue(['/project/.maestro/home.yml']);
|
||||
});
|
||||
|
||||
function createStep(overrides?: { callInputs?: Record<string, unknown> }) {
|
||||
const ctx = {
|
||||
runtimeApi: { uploadArtifact: mockUploadArtifact },
|
||||
};
|
||||
const fn = createInternalEasMaestroTestFunction(ctx as any);
|
||||
return fn.createBuildStepFromFunctionCall(
|
||||
createGlobalContextMock({
|
||||
logger: createMockLogger(),
|
||||
}),
|
||||
{
|
||||
callInputs: {
|
||||
platform: 'ios',
|
||||
flow_paths: JSON.stringify(['.maestro']),
|
||||
...overrides?.callInputs,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
it('sets junit_report_directory output when output_format is junit', async () => {
|
||||
const step = createStep({
|
||||
callInputs: { output_format: 'junit' },
|
||||
});
|
||||
await step.executeAsync();
|
||||
|
||||
const junitDir = step.getOutputValueByName('junit_report_directory');
|
||||
expect(junitDir).toMatch(/maestro-reports-/);
|
||||
});
|
||||
|
||||
it('does not set junit_report_directory output when output_format is not junit', async () => {
|
||||
const step = createStep();
|
||||
await step.executeAsync();
|
||||
|
||||
const junitDir = step.getOutputValueByName('junit_report_directory');
|
||||
expect(junitDir).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not set junit_report_directory when function throws before reaching output code', async () => {
|
||||
mockedIosUtils.getAvailableDevicesAsync.mockResolvedValue([]);
|
||||
|
||||
const step = createStep({
|
||||
callInputs: { output_format: 'junit' },
|
||||
});
|
||||
|
||||
try {
|
||||
await step.executeAsync();
|
||||
} catch {
|
||||
// Expected - no booted device found
|
||||
}
|
||||
|
||||
const junitDir = step.getOutputValueByName('junit_report_directory');
|
||||
expect(junitDir).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,594 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import {
|
||||
parseFlowMetadata,
|
||||
parseJUnitTestCases,
|
||||
parseMaestroResults,
|
||||
} from '../maestroResultParser';
|
||||
|
||||
describe(parseFlowMetadata, () => {
|
||||
it('parses valid ai-*.json', async () => {
|
||||
vol.fromJSON({
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/Users/expo/workingdir/build/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await parseFlowMetadata('/tests/2026-01-28_055409/ai-home.json');
|
||||
expect(result).toEqual({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/Users/expo/workingdir/build/.maestro/home.yml',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null when flow_name is missing', async () => {
|
||||
vol.fromJSON({
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_file_path: '/Users/expo/workingdir/build/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await parseFlowMetadata('/tests/2026-01-28_055409/ai-home.json');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when flow_file_path is missing', async () => {
|
||||
vol.fromJSON({
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await parseFlowMetadata('/tests/2026-01-28_055409/ai-home.json');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for invalid JSON', async () => {
|
||||
vol.fromJSON({
|
||||
'/tests/2026-01-28_055409/ai-home.json': 'not json',
|
||||
});
|
||||
|
||||
const result = await parseFlowMetadata('/tests/2026-01-28_055409/ai-home.json');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe(parseMaestroResults, () => {
|
||||
it('parses JUnit results and enriches with ai-*.json metadata', async () => {
|
||||
vol.fromJSON({
|
||||
// JUnit XML (primary data)
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="2" failures="1">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.5" status="SUCCESS"/>',
|
||||
' <testcase id="login" name="login" classname="login" time="5.0" status="ERROR">',
|
||||
' <failure>Tap failed</failure>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
// Debug output (for flow_file_path)
|
||||
'/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',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
name: 'home',
|
||||
path: '.maestro/home.yml',
|
||||
status: 'passed',
|
||||
errorMessage: null,
|
||||
duration: 10500,
|
||||
retryCount: 0,
|
||||
tags: [],
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
name: 'login',
|
||||
path: '.maestro/login.yml',
|
||||
status: 'failed',
|
||||
errorMessage: 'Tap failed',
|
||||
duration: 5000,
|
||||
retryCount: 0,
|
||||
tags: [],
|
||||
properties: {},
|
||||
},
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('calculates retryCount from timestamp directory occurrences', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
// Two timestamp dirs = 1 retry
|
||||
'/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_055420/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
expect(results[0].retryCount).toBe(1);
|
||||
});
|
||||
|
||||
it('uses flow name as fallback path when ai-*.json not found', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
// No ai-*.json files
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
expect(results[0].path).toBe('home');
|
||||
expect(results[0].retryCount).toBe(0);
|
||||
});
|
||||
|
||||
it('returns empty array when no JUnit files found', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/.gitkeep': '',
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('extracts tags from JUnit properties and separates from other properties', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS">',
|
||||
' <properties>',
|
||||
' <property name="env" value="staging"/>',
|
||||
' <property name="tags" value="e2e, smoke"/>',
|
||||
' </properties>',
|
||||
' </testcase>',
|
||||
' </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',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
expect(results[0].tags).toEqual(['e2e', 'smoke']);
|
||||
expect(results[0].properties).toEqual({ env: 'staging' });
|
||||
});
|
||||
|
||||
it('handles reuse_devices=true (junit_report_directory == tests_directory)', async () => {
|
||||
vol.fromJSON({
|
||||
// Same directory for both JUnit and debug output
|
||||
'/maestro-tests/android-maestro-junit.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/maestro-tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/maestro-tests', '/maestro-tests', '/root/project');
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'home',
|
||||
path: '.maestro/home.yml',
|
||||
status: 'passed',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles reuse_devices=false (separate junit_report_directory)', async () => {
|
||||
vol.fromJSON({
|
||||
// JUnit in temp dir (per-flow files)
|
||||
'/tmp/maestro-reports-abc123/junit-report-flow-1.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/tmp/maestro-reports-abc123/junit-report-flow-2.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>Failed</failure>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
// Debug output in default location
|
||||
'/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',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults(
|
||||
'/tmp/maestro-reports-abc123',
|
||||
'/tests',
|
||||
'/root/project'
|
||||
);
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'home', path: '.maestro/home.yml', status: 'passed' }),
|
||||
expect.objectContaining({ name: 'login', path: '.maestro/login.yml', status: 'failed' }),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('uses raw path when flow_file_path is outside project_root', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/somewhere/else/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
expect(results[0].path).toBe('/somewhere/else/.maestro/home.yml');
|
||||
});
|
||||
|
||||
it('filters out non-timestamp directories when scanning debug output', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.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/not-a-timestamp/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
// Only 1 occurrence (not-a-timestamp dir should be ignored)
|
||||
expect(results[0].retryCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe(parseJUnitTestCases, () => {
|
||||
it('parses a single testcase with SUCCESS status', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" device="emulator-5554" tests="1" failures="0" time="10.5">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.5" status="SUCCESS">',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results).toEqual([
|
||||
{
|
||||
name: 'home',
|
||||
status: 'passed',
|
||||
duration: 10500,
|
||||
errorMessage: null,
|
||||
tags: [],
|
||||
properties: {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('parses a failed testcase with failure message', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.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>Element not visible</failure>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results).toEqual([
|
||||
{
|
||||
name: 'login',
|
||||
status: 'failed',
|
||||
duration: 5000,
|
||||
errorMessage: 'Element not visible',
|
||||
tags: [],
|
||||
properties: {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('extracts properties from testcase', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS">',
|
||||
' <properties>',
|
||||
' <property name="env" value="staging"/>',
|
||||
' <property name="priority" value="high"/>',
|
||||
' </properties>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].properties).toEqual({ env: 'staging', priority: 'high' });
|
||||
});
|
||||
|
||||
it('extracts tags from "tags" property as comma-separated values', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS">',
|
||||
' <properties>',
|
||||
' <property name="tags" value="smoke, critical, auth"/>',
|
||||
' <property name="priority" value="high"/>',
|
||||
' </properties>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].tags).toEqual(['smoke', 'critical', 'auth']);
|
||||
expect(results[0].properties).toEqual({ priority: 'high' });
|
||||
});
|
||||
|
||||
it('returns empty tags when no "tags" property exists (older Maestro)', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS">',
|
||||
' <properties>',
|
||||
' <property name="priority" value="high"/>',
|
||||
' </properties>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].tags).toEqual([]);
|
||||
expect(results[0].properties).toEqual({ priority: 'high' });
|
||||
});
|
||||
|
||||
it('parses multiple testcases across multiple testsuites (shards)', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.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="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
' <testsuite name="Test Suite" device="emulator-5556" tests="1" failures="1">',
|
||||
' <testcase id="login" name="login" classname="login" time="5.0" status="ERROR">',
|
||||
' <failure>Tap failed</failure>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'home', status: 'passed' }),
|
||||
expect.objectContaining({ name: 'login', status: 'failed', errorMessage: 'Tap failed' }),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('parses multiple JUnit XML files in the same directory', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/junit-report-flow-1.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/junit/junit-report-flow-2.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>Tap failed</failure>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('handles missing time attribute (defaults to 0)', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].duration).toBe(0);
|
||||
});
|
||||
|
||||
it('handles invalid time attribute (defaults to 0)', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="not_a_number" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].duration).toBe(0);
|
||||
});
|
||||
|
||||
it('uses @_status attribute for pass/fail (not <failure> presence)', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.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"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].status).toBe('failed');
|
||||
expect(results[0].errorMessage).toBeNull();
|
||||
});
|
||||
|
||||
it('extracts error message from <error> element', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.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">',
|
||||
' <error>Runtime exception occurred</error>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].status).toBe('failed');
|
||||
expect(results[0].errorMessage).toBe('Runtime exception occurred');
|
||||
});
|
||||
|
||||
it('returns empty array when directory does not exist', async () => {
|
||||
const results = await parseJUnitTestCases('/nonexistent');
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array when no XML files found', async () => {
|
||||
vol.fromJSON({ '/junit/.gitkeep': '' });
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('skips invalid XML files gracefully', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/bad.xml': 'not xml at all',
|
||||
'/junit/good.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results).toEqual([expect.objectContaining({ name: 'home', status: 'passed' })]);
|
||||
});
|
||||
|
||||
it('handles testcase with no properties element', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].properties).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,273 @@
|
||||
import { Client } from '@urql/core';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { createGlobalContextMock } from '../../../__tests__/utils/context';
|
||||
import { createMockLogger } from '../../../__tests__/utils/logger';
|
||||
import { createReportMaestroTestResultsFunction } from '../reportMaestroTestResults';
|
||||
|
||||
const JUNIT_PASS = [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n');
|
||||
|
||||
const JUNIT_FAIL = [
|
||||
'<?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');
|
||||
|
||||
const FLOW_AI = JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
});
|
||||
|
||||
describe(createReportMaestroTestResultsFunction, () => {
|
||||
let mockMutationFn: jest.Mock;
|
||||
let mockGraphqlClient: Client;
|
||||
|
||||
beforeEach(() => {
|
||||
mockMutationFn = jest.fn();
|
||||
mockGraphqlClient = {
|
||||
mutation: jest.fn().mockReturnValue({
|
||||
toPromise: mockMutationFn,
|
||||
}),
|
||||
} as unknown as Client;
|
||||
});
|
||||
|
||||
function createStep(overrides?: {
|
||||
callInputs?: Record<string, unknown>;
|
||||
staticContextContent?: Record<string, unknown>;
|
||||
env?: Record<string, string>;
|
||||
}) {
|
||||
const ctx = { graphqlClient: mockGraphqlClient };
|
||||
const fn = createReportMaestroTestResultsFunction(ctx as any);
|
||||
const globalCtx = createGlobalContextMock({
|
||||
logger: createMockLogger(),
|
||||
projectTargetDirectory: '/root/project',
|
||||
staticContextContent: {
|
||||
expoApiServerURL: 'https://api.expo.test',
|
||||
job: { secrets: { robotAccessToken: 'test-token' } },
|
||||
...overrides?.staticContextContent,
|
||||
},
|
||||
});
|
||||
globalCtx.updateEnv(overrides?.env ?? { __WORKFLOW_JOB_ID: 'job-uuid-123' });
|
||||
return fn.createBuildStepFromFunctionCall(globalCtx, {
|
||||
callInputs: {
|
||||
junit_report_directory: '/junit',
|
||||
tests_directory: '/tests',
|
||||
...overrides?.callInputs,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
it('parses JUnit results and calls GraphQL mutation', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': JUNIT_PASS,
|
||||
'/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
mockMutationFn.mockResolvedValue({
|
||||
data: {
|
||||
workflowDeviceTestCaseResult: {
|
||||
createWorkflowDeviceTestCaseResults: [{ id: 'id-1' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await createStep().executeAsync();
|
||||
|
||||
expect(mockGraphqlClient.mutation).toHaveBeenCalledTimes(1);
|
||||
const [, variables] = (mockGraphqlClient.mutation as jest.Mock).mock.calls[0];
|
||||
expect(variables.input.workflowJobId).toBe('job-uuid-123');
|
||||
expect(variables.input.testCaseResults).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'home',
|
||||
path: '.maestro/home.yml',
|
||||
status: 'PASSED',
|
||||
errorMessage: null,
|
||||
duration: 10000,
|
||||
retryCount: 0,
|
||||
properties: {},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('sends tags and properties from JUnit XML', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS">',
|
||||
' <properties>',
|
||||
' <property name="testCaseId" value="TC-001"/>',
|
||||
' <property name="priority" value="high"/>',
|
||||
' <property name="tags" value="e2e"/>',
|
||||
' </properties>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
mockMutationFn.mockResolvedValue({
|
||||
data: {
|
||||
workflowDeviceTestCaseResult: {
|
||||
createWorkflowDeviceTestCaseResults: [{ id: 'id-1' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await createStep().executeAsync();
|
||||
|
||||
const [, variables] = (mockGraphqlClient.mutation as jest.Mock).mock.calls[0];
|
||||
expect(variables.input.testCaseResults[0].tags).toEqual(['e2e']);
|
||||
expect(variables.input.testCaseResults[0].properties).toEqual({
|
||||
testCaseId: 'TC-001',
|
||||
priority: 'high',
|
||||
});
|
||||
});
|
||||
|
||||
it('reports failed flow with correct status and errorMessage', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': JUNIT_FAIL,
|
||||
'/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
mockMutationFn.mockResolvedValue({
|
||||
data: {
|
||||
workflowDeviceTestCaseResult: {
|
||||
createWorkflowDeviceTestCaseResults: [{ id: 'id-1' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await createStep().executeAsync();
|
||||
|
||||
const [, variables] = (mockGraphqlClient.mutation as jest.Mock).mock.calls[0];
|
||||
expect(variables.input.testCaseResults[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
status: 'FAILED',
|
||||
errorMessage: 'Tap failed',
|
||||
duration: 5000,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// robotAccessToken guard removed — Generic.JobZ requires robotAccessToken (z.string(),
|
||||
// not optional), so it's always present in workflow jobs. graphqlClient is already
|
||||
// initialized with the token in BuildContext constructor.
|
||||
|
||||
it('skips report when junit_report_directory is empty string (upstream step failed)', async () => {
|
||||
const step = createStep({
|
||||
callInputs: { junit_report_directory: '' },
|
||||
});
|
||||
await step.executeAsync();
|
||||
expect(mockGraphqlClient.mutation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips report when __WORKFLOW_JOB_ID env is not set', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': JUNIT_PASS,
|
||||
'/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
const step = createStep({ env: {} });
|
||||
await step.executeAsync();
|
||||
expect(mockGraphqlClient.mutation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips report when no JUnit files found', async () => {
|
||||
const step = createStep();
|
||||
await step.executeAsync();
|
||||
expect(mockGraphqlClient.mutation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not throw when GraphQL returns error', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': JUNIT_PASS,
|
||||
'/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
mockMutationFn.mockResolvedValue({
|
||||
error: { message: 'Something went wrong' },
|
||||
});
|
||||
|
||||
await createStep().executeAsync();
|
||||
});
|
||||
|
||||
it('does not throw when mutation rejects', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': JUNIT_PASS,
|
||||
'/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
mockMutationFn.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await createStep().executeAsync();
|
||||
});
|
||||
|
||||
it('skips report when duplicate testcase names found', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="2" failures="0">',
|
||||
' <testcase id="login" name="login" classname="login" time="10.0" status="SUCCESS"/>',
|
||||
' <testcase id="login" name="login" classname="login" time="5.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/tests/2026-01-28_055409/ai-login.json': JSON.stringify({
|
||||
flow_name: 'login',
|
||||
flow_file_path: '/root/project/.maestro/login.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
await createStep().executeAsync();
|
||||
expect(mockGraphqlClient.mutation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses default directories when inputs are not provided', async () => {
|
||||
vol.fromJSON({
|
||||
'/home/expo/.maestro/tests/report.xml': JUNIT_PASS,
|
||||
'/home/expo/.maestro/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
const ctx = { graphqlClient: mockGraphqlClient };
|
||||
const fn = createReportMaestroTestResultsFunction(ctx as any);
|
||||
const globalCtx = createGlobalContextMock({
|
||||
logger: createMockLogger(),
|
||||
projectTargetDirectory: '/root/project',
|
||||
staticContextContent: {
|
||||
expoApiServerURL: 'https://api.expo.test',
|
||||
job: { secrets: { robotAccessToken: 'test-token' } },
|
||||
},
|
||||
});
|
||||
globalCtx.updateEnv({ __WORKFLOW_JOB_ID: 'job-uuid-123', HOME: '/home/expo' });
|
||||
|
||||
mockMutationFn.mockResolvedValue({
|
||||
data: {
|
||||
workflowDeviceTestCaseResult: {
|
||||
createWorkflowDeviceTestCaseResults: [{ id: 'id-1' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const step = fn.createBuildStepFromFunctionCall(globalCtx, {
|
||||
callInputs: {},
|
||||
});
|
||||
await step.executeAsync();
|
||||
expect(mockGraphqlClient.mutation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -81,6 +81,10 @@ export function createInternalEasMaestroTestFunction(ctx: CustomBuildContext): B
|
||||
id: 'test_reports_artifact_id',
|
||||
required: false,
|
||||
}),
|
||||
BuildStepOutput.createProvider({
|
||||
id: 'junit_report_directory',
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
fn: async (stepCtx, { inputs: _inputs, env, outputs }) => {
|
||||
// inputs come in form of { value: unknown }. Here we parse them into a typed and validated object.
|
||||
@@ -348,6 +352,10 @@ export function createInternalEasMaestroTestFunction(ctx: CustomBuildContext): B
|
||||
}
|
||||
}
|
||||
|
||||
if (output_format === 'junit') {
|
||||
outputs.junit_report_directory.set(maestroReportsDir);
|
||||
}
|
||||
|
||||
const generatedDeviceLogs = await fs.promises.readdir(deviceLogsDir);
|
||||
if (generatedDeviceLogs.length === 0) {
|
||||
stepCtx.logger.warn('No device logs were successfully collected.');
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { z } from 'zod';
|
||||
|
||||
export interface MaestroFlowResult {
|
||||
name: string;
|
||||
path: string;
|
||||
status: 'passed' | 'failed';
|
||||
errorMessage: string | null;
|
||||
duration: number; // milliseconds
|
||||
retryCount: number;
|
||||
tags: string[];
|
||||
properties: Record<string, string>;
|
||||
}
|
||||
|
||||
// Maestro's TestDebugReporter creates timestamped directories, e.g. "2024-06-15_143022"
|
||||
const TIMESTAMP_DIR_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{6}$/;
|
||||
|
||||
export function extractFlowKey(filename: string, prefix: string): string | null {
|
||||
const match = filename.match(new RegExp(`^${prefix}-(.+)\\.json$`));
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
export interface JUnitTestCaseResult {
|
||||
name: string;
|
||||
status: 'passed' | 'failed';
|
||||
duration: number; // milliseconds
|
||||
errorMessage: string | null;
|
||||
tags: string[];
|
||||
properties: Record<string, string>;
|
||||
}
|
||||
|
||||
const xmlParser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: '@_',
|
||||
// Ensure single-element arrays are always arrays
|
||||
isArray: name => ['testsuite', 'testcase', 'property'].includes(name),
|
||||
});
|
||||
|
||||
export async function parseJUnitTestCases(junitDirectory: string): Promise<JUnitTestCaseResult[]> {
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = await fs.readdir(junitDirectory);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const xmlFiles = entries.filter(f => f.endsWith('.xml'));
|
||||
if (xmlFiles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results: JUnitTestCaseResult[] = [];
|
||||
|
||||
for (const xmlFile of xmlFiles) {
|
||||
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;
|
||||
}
|
||||
|
||||
const FlowMetadataFileSchema = z.object({
|
||||
flow_name: z.string(),
|
||||
flow_file_path: z.string(),
|
||||
});
|
||||
|
||||
type FlowMetadata = z.output<typeof FlowMetadataFileSchema>;
|
||||
|
||||
/**
|
||||
* Parses an `ai-*.json` file produced by Maestro's TestDebugReporter.
|
||||
*
|
||||
* The file contains:
|
||||
* - `flow_name`: derived from the YAML `config.name` field if present, otherwise
|
||||
* the flow filename without extension.
|
||||
* See: https://github.com/mobile-dev-inc/Maestro/blob/c0e95fd/maestro-cli/src/main/java/maestro/cli/runner/TestRunner.kt#L70
|
||||
* - `flow_file_path`: absolute path to the original flow YAML file.
|
||||
* - `outputs`: screenshot defect data (unused here).
|
||||
*
|
||||
* Filename format: `ai-(flowName).json` where `/` in flowName is replaced with `_`.
|
||||
* See: https://github.com/mobile-dev-inc/Maestro/blob/c0e95fd/maestro-cli/src/main/java/maestro/cli/report/TestDebugReporter.kt#L67
|
||||
*/
|
||||
export async function parseFlowMetadata(filePath: string): Promise<FlowMetadata | null> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const data = JSON.parse(content);
|
||||
return FlowMetadataFileSchema.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseMaestroResults(
|
||||
junitDirectory: string,
|
||||
testsDirectory: string,
|
||||
projectRoot: string
|
||||
): Promise<MaestroFlowResult[]> {
|
||||
// 1. Parse JUnit XML files (primary source)
|
||||
const junitResults = await parseJUnitTestCases(junitDirectory);
|
||||
if (junitResults.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2. Parse ai-*.json from debug output for flow_file_path + retryCount
|
||||
const flowPathMap = new Map<string, string>(); // flowName → flowFilePath
|
||||
const flowOccurrences = new Map<string, number>(); // flowName → count
|
||||
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = await fs.readdir(testsDirectory);
|
||||
} catch {
|
||||
entries = [];
|
||||
}
|
||||
|
||||
const timestampDirs = entries.filter(name => TIMESTAMP_DIR_PATTERN.test(name)).sort();
|
||||
|
||||
for (const dir of timestampDirs) {
|
||||
const dirPath = path.join(testsDirectory, dir);
|
||||
let files: string[];
|
||||
try {
|
||||
files = await fs.readdir(dirPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const flowKey = extractFlowKey(file, 'ai');
|
||||
if (!flowKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const metadata = await parseFlowMetadata(path.join(dirPath, file));
|
||||
if (!metadata) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track latest path (last timestamp dir wins)
|
||||
flowPathMap.set(metadata.flow_name, metadata.flow_file_path);
|
||||
|
||||
// Count occurrences for retryCount
|
||||
flowOccurrences.set(metadata.flow_name, (flowOccurrences.get(metadata.flow_name) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Merge: JUnit results + ai-*.json metadata
|
||||
const results: MaestroFlowResult[] = [];
|
||||
|
||||
for (const junit of junitResults) {
|
||||
const flowFilePath = flowPathMap.get(junit.name);
|
||||
const relativePath = flowFilePath
|
||||
? await relativizePathAsync(flowFilePath, projectRoot)
|
||||
: junit.name; // fallback: use flow name if ai-*.json not found
|
||||
|
||||
const occurrences = flowOccurrences.get(junit.name) ?? 0;
|
||||
const retryCount = Math.max(0, occurrences - 1);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function relativizePathAsync(flowFilePath: string, projectRoot: string): Promise<string> {
|
||||
if (!path.isAbsolute(flowFilePath)) {
|
||||
return flowFilePath;
|
||||
}
|
||||
|
||||
// Resolve symlinks (e.g., /tmp -> /private/tmp on macOS) for consistent comparison
|
||||
let resolvedRoot = projectRoot;
|
||||
let resolvedFlow = flowFilePath;
|
||||
try {
|
||||
resolvedRoot = await fs.realpath(projectRoot);
|
||||
} catch {}
|
||||
try {
|
||||
resolvedFlow = await fs.realpath(flowFilePath);
|
||||
} catch {}
|
||||
|
||||
const relative = path.relative(resolvedRoot, resolvedFlow);
|
||||
if (relative.startsWith('..')) {
|
||||
return flowFilePath;
|
||||
}
|
||||
return relative;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { BuildFunction, BuildStepInput, BuildStepInputValueTypeName } from '@expo/steps';
|
||||
import { graphql } from 'gql.tada';
|
||||
|
||||
import { MaestroFlowResult, parseMaestroResults } from './maestroResultParser';
|
||||
import { CustomBuildContext } from '../../customBuildContext';
|
||||
|
||||
const CREATE_MUTATION = graphql(`
|
||||
mutation CreateWorkflowDeviceTestCaseResults($input: CreateWorkflowDeviceTestCaseResultsInput!) {
|
||||
workflowDeviceTestCaseResult {
|
||||
createWorkflowDeviceTestCaseResults(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const FLOW_STATUS_TO_TEST_CASE_RESULT_STATUS: Record<string, 'PASSED' | 'FAILED' | undefined> = {
|
||||
passed: 'PASSED',
|
||||
failed: 'FAILED',
|
||||
} satisfies Record<MaestroFlowResult['status'], 'PASSED' | 'FAILED'>;
|
||||
|
||||
export function createReportMaestroTestResultsFunction(ctx: CustomBuildContext): BuildFunction {
|
||||
return new BuildFunction({
|
||||
namespace: 'eas',
|
||||
id: 'report_maestro_test_results',
|
||||
name: 'Report Maestro Test Results',
|
||||
__metricsId: 'eas/report_maestro_test_results',
|
||||
inputProviders: [
|
||||
BuildStepInput.createProvider({
|
||||
id: 'junit_report_directory',
|
||||
required: false,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
defaultValue: '${{ env.HOME }}/.maestro/tests',
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
id: 'tests_directory',
|
||||
required: false,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
defaultValue: '${{ env.HOME }}/.maestro/tests',
|
||||
}),
|
||||
],
|
||||
fn: async (stepsCtx, { inputs }) => {
|
||||
const { logger } = stepsCtx;
|
||||
const workflowJobId = stepsCtx.global.env.__WORKFLOW_JOB_ID;
|
||||
if (!workflowJobId) {
|
||||
logger.info('Not running in a workflow job, skipping test results report');
|
||||
return;
|
||||
}
|
||||
const junitDirectory = (inputs.junit_report_directory.value as string | undefined) ?? '';
|
||||
if (!junitDirectory) {
|
||||
logger.info('No JUnit directory provided, skipping test results report');
|
||||
return;
|
||||
}
|
||||
const testsDirectory = inputs.tests_directory.value as string;
|
||||
|
||||
try {
|
||||
const flowResults = await parseMaestroResults(
|
||||
junitDirectory,
|
||||
testsDirectory,
|
||||
stepsCtx.workingDirectory
|
||||
);
|
||||
if (flowResults.length === 0) {
|
||||
logger.info('No maestro test results found, skipping report');
|
||||
return;
|
||||
}
|
||||
|
||||
// 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: ${[...new Set(duplicates)].join(
|
||||
', '
|
||||
)}. Skipping report. Ensure each Maestro flow has a unique name.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const testCaseResults = flowResults.flatMap(f => {
|
||||
const status = FLOW_STATUS_TO_TEST_CASE_RESULT_STATUS[f.status];
|
||||
if (!status) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: f.name,
|
||||
path: f.path,
|
||||
status,
|
||||
errorMessage: f.errorMessage,
|
||||
duration: f.duration,
|
||||
retryCount: f.retryCount,
|
||||
tags: f.tags,
|
||||
properties: f.properties,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const result = await ctx.graphqlClient
|
||||
.mutation(CREATE_MUTATION, {
|
||||
input: {
|
||||
workflowJobId,
|
||||
testCaseResults,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
if (result.error) {
|
||||
logger.error({ error: result.error }, 'GraphQL error creating test case results');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Reported ${testCaseResults.length} test case result(s).`);
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'Failed to create test case results');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-eas-build-function",
|
||||
"version": "18.0.2",
|
||||
"version": "18.0.3",
|
||||
"description": "Create functions for use in EAS Build custom builds.",
|
||||
"keywords": [
|
||||
"expo",
|
||||
@@ -32,7 +32,7 @@
|
||||
"build:dev": "ncc build ./src/index.ts -o build/",
|
||||
"build": "ncc build ./src/index.ts -o build/ --minify --no-cache --no-source-map-register",
|
||||
"typecheck": "tsc",
|
||||
"clean": "rimraf ./build/ *.tsbuildinfo"
|
||||
"clean": "rimraf ./build/ \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/steps": "18.0.2"
|
||||
|
||||
+92
-90
@@ -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.0.2/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.0.6/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.0.2/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.0.6/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.0.2/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.0.6/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.0.2/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.0.6/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.0.2/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.0.6/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.0.2/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.0.6/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.0.2/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.0.6/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.0.2/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.0.6/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.0.2/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.0.6/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.0.2/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.0.6/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -425,7 +425,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -444,7 +444,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -461,7 +461,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.0.2/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.0.6/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.0.2/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.0.6/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
|
||||
## `eas build:dev`
|
||||
|
||||
@@ -500,7 +500,7 @@ DESCRIPTION
|
||||
run dev client simulator/emulator build with matching fingerprint or create a new one
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
|
||||
## `eas build:download`
|
||||
|
||||
@@ -521,7 +521,7 @@ DESCRIPTION
|
||||
download simulator/emulator builds for a given fingerprint hash
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/build/download.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -556,7 +556,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.0.2/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.0.6/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -608,7 +608,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.0.2/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.0.6/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -637,7 +637,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.0.2/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.0.6/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -663,7 +663,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.0.2/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.0.6/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -717,7 +717,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.0.2/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.0.6/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
|
||||
## `eas build:version:set`
|
||||
|
||||
@@ -736,7 +736,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.0.2/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.0.6/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -755,7 +755,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.0.2/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.0.6/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -772,7 +772,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.0.2/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.0.6/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -793,7 +793,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:delete [NAME]`
|
||||
|
||||
@@ -814,7 +814,7 @@ DESCRIPTION
|
||||
Delete a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -836,7 +836,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.0.2/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.0.6/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -856,7 +856,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:pause [NAME]`
|
||||
|
||||
@@ -878,7 +878,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.0.2/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.0.6/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
|
||||
## `eas channel:resume [NAME]`
|
||||
|
||||
@@ -900,7 +900,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.0.2/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.0.6/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
@@ -933,7 +933,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.0.2/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.0.6/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -956,7 +956,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -977,7 +977,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.0.2/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.0.6/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -994,7 +994,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
|
||||
## `eas credentials:configure-build`
|
||||
|
||||
@@ -1012,7 +1012,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.0.2/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.0.6/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
|
||||
## `eas deploy [options]`
|
||||
|
||||
@@ -1040,7 +1040,7 @@ ALIASES
|
||||
$ eas worker:deploy
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
|
||||
## `eas deploy:alias`
|
||||
|
||||
@@ -1065,7 +1065,7 @@ ALIASES
|
||||
$ eas deploy:promote
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
|
||||
## `eas deploy:alias:delete [ALIAS_NAME]`
|
||||
|
||||
@@ -1086,7 +1086,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.0.2/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.0.6/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
|
||||
## `eas deploy:delete [DEPLOYMENT_ID]`
|
||||
|
||||
@@ -1107,7 +1107,7 @@ ALIASES
|
||||
$ eas worker:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
|
||||
## `eas deploy:promote`
|
||||
|
||||
@@ -1144,7 +1144,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.0.2/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.0.6/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -1164,7 +1164,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.0.2/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.0.6/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -1185,7 +1185,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.0.2/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.0.6/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:rename`
|
||||
|
||||
@@ -1206,7 +1206,7 @@ DESCRIPTION
|
||||
rename a registered device
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -1220,7 +1220,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.0.2/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.0.6/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -1234,7 +1234,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.0.2/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.0.6/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas env:create [ENVIRONMENT]`
|
||||
|
||||
@@ -1264,7 +1264,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.0.2/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.0.6/packages/eas-cli/src/commands/env/create.ts)_
|
||||
|
||||
## `eas env:delete [ENVIRONMENT]`
|
||||
|
||||
@@ -1289,7 +1289,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.0.2/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.0.6/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
|
||||
## `eas env:exec ENVIRONMENT BASH_COMMAND`
|
||||
|
||||
@@ -1311,7 +1311,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.0.2/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.0.6/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
|
||||
## `eas env:get [ENVIRONMENT]`
|
||||
|
||||
@@ -1336,7 +1336,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.0.2/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.0.6/packages/eas-cli/src/commands/env/get.ts)_
|
||||
|
||||
## `eas env:list [ENVIRONMENT]`
|
||||
|
||||
@@ -1362,7 +1362,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.0.2/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.0.6/packages/eas-cli/src/commands/env/list.ts)_
|
||||
|
||||
## `eas env:pull [ENVIRONMENT]`
|
||||
|
||||
@@ -1384,7 +1384,7 @@ DESCRIPTION
|
||||
pull environment variables for the selected environment to .env file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
|
||||
## `eas env:push [ENVIRONMENT]`
|
||||
|
||||
@@ -1406,7 +1406,7 @@ DESCRIPTION
|
||||
push environment variables from .env file to the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/env/push.ts)_
|
||||
|
||||
## `eas env:update [ENVIRONMENT]`
|
||||
|
||||
@@ -1438,7 +1438,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.0.2/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.0.6/packages/eas-cli/src/commands/env/update.ts)_
|
||||
|
||||
## `eas fingerprint:compare [HASH1] [HASH2]`
|
||||
|
||||
@@ -1482,7 +1482,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.0.2/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.0.6/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
|
||||
## `eas fingerprint:generate`
|
||||
|
||||
@@ -1512,7 +1512,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.0.2/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.0.6/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -1621,7 +1621,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.0.2/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.0.6/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -1638,7 +1638,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.0.2/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.0.6/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -1655,7 +1655,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.0.2/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.0.6/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas new [PATH]`
|
||||
|
||||
@@ -1706,7 +1706,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.0.2/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.0.6/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -1720,7 +1720,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.0.2/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.0.6/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -1743,7 +1743,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas project:new [PATH]`
|
||||
|
||||
@@ -1766,7 +1766,7 @@ ALIASES
|
||||
$ eas new
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/project/new.ts)_
|
||||
|
||||
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
|
||||
|
||||
@@ -1784,7 +1784,7 @@ ALIASES
|
||||
$ eas onboarding
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -1819,7 +1819,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.0.2/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.0.6/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1841,7 +1841,8 @@ FLAGS
|
||||
--emit-metadata Emit "eas-update-metadata.json" in the bundle folder with detailed information about
|
||||
the generated updates
|
||||
--environment=<value> Environment to use for the server-side defined EAS environment variables during
|
||||
command execution, e.g. "production", "preview", "development"
|
||||
command execution, e.g. "production", "preview", "development". Required for
|
||||
projects using Expo SDK 55 or greater.
|
||||
--input-dir=<value> [default: dist] Location of the bundle
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
@@ -1859,7 +1860,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1872,14 +1873,15 @@ USAGE
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all) [default: all] Platform to configure
|
||||
--environment=<value> Environment to use for the server-side defined EAS environment variables during
|
||||
command execution, e.g. "production", "preview", "development"
|
||||
command execution, e.g. "production", "preview", "development". Required for
|
||||
projects using Expo SDK 55 or greater.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
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.0.2/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.0.6/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1900,7 +1902,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.0.2/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.0.6/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:edit [GROUPID]`
|
||||
|
||||
@@ -1924,7 +1926,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.0.2/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.0.6/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -1949,7 +1951,7 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
@@ -1985,7 +1987,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.0.2/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.0.6/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:revert-update-rollout`
|
||||
|
||||
@@ -2012,7 +2014,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.0.2/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.0.6/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
|
||||
## `eas update:roll-back-to-embedded`
|
||||
|
||||
@@ -2040,7 +2042,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.0.2/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.0.6/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
|
||||
## `eas update:rollback`
|
||||
|
||||
@@ -2061,7 +2063,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.0.2/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.0.6/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -2081,7 +2083,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas upload`
|
||||
|
||||
@@ -2102,7 +2104,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.0.2/packages/eas-cli/src/commands/upload.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.0.6/packages/eas-cli/src/commands/upload.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -2123,7 +2125,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -2143,7 +2145,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -2161,7 +2163,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -2183,7 +2185,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -2200,7 +2202,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
@@ -2320,7 +2322,7 @@ DESCRIPTION
|
||||
to cancel.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.0.2/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.0.6/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
|
||||
## `eas workflow:create [NAME]`
|
||||
|
||||
@@ -2340,7 +2342,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.0.2/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.0.6/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
|
||||
## `eas workflow:logs [ID]`
|
||||
|
||||
@@ -2364,7 +2366,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.0.2/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.0.6/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
|
||||
## `eas workflow:run [FILE]`
|
||||
|
||||
@@ -2404,7 +2406,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.0.2/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.0.6/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
|
||||
## `eas workflow:runs`
|
||||
|
||||
@@ -2429,7 +2431,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.0.2/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.0.6/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
|
||||
## `eas workflow:status [WORKFLOW_RUN_ID]`
|
||||
|
||||
@@ -2457,7 +2459,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.0.2/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.0.6/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
|
||||
## `eas workflow:validate PATH`
|
||||
|
||||
@@ -2477,7 +2479,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.0.2/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.0.6/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
|
||||
## `eas workflow:view [ID]`
|
||||
|
||||
@@ -2499,5 +2501,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.0.2/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.0.6/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "18.0.2",
|
||||
"version": "18.0.6",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -2,6 +2,6 @@ const spawn = require('@expo/spawn-async');
|
||||
|
||||
(async () => {
|
||||
if (!process.env.CLI_SIZE_CHECK) {
|
||||
await spawn('yarn', ['oclif', 'manifest'], { stdio: 'inherit' });
|
||||
await spawn('yarn', ['run', '-T', 'oclif', 'manifest'], { stdio: 'inherit' });
|
||||
}
|
||||
})();
|
||||
@@ -67,7 +67,7 @@ export const EasJsonOnlyFlag = {
|
||||
export const EasUpdateEnvironmentFlag = {
|
||||
environment: Flags.string({
|
||||
description:
|
||||
'Environment to use for the server-side defined EAS environment variables during command execution, e.g. "production", "preview", "development"',
|
||||
'Environment to use for the server-side defined EAS environment variables during command execution, e.g. "production", "preview", "development". Required for projects using Expo SDK 55 or greater.',
|
||||
required: false,
|
||||
default: undefined,
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,656 @@
|
||||
import { App, User, UserRole } from '@expo/apple-utils';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { Analytics } from '../analytics/AnalyticsManager';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { saveProjectIdToAppConfigAsync } from '../commandUtils/context/contextUtils/getProjectIdAsync';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { AppStoreApiKeyPurpose } from '../credentials/ios/actions/AscApiKeyUtils';
|
||||
import { getAppFromContextAsync } from '../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { SetUpAscApiKey } from '../credentials/ios/actions/SetUpAscApiKey';
|
||||
import { SetUpBuildCredentials } from '../credentials/ios/actions/SetUpBuildCredentials';
|
||||
import { SetUpPushKey } from '../credentials/ios/actions/SetUpPushKey';
|
||||
import { ensureAppExistsAsync } from '../credentials/ios/appstore/ensureAppExists';
|
||||
import { Target } from '../credentials/ios/types';
|
||||
import {
|
||||
WorkflowJobStatus,
|
||||
WorkflowProjectSourceType,
|
||||
WorkflowRunStatus,
|
||||
} from '../graphql/generated';
|
||||
import { AppMutation } from '../graphql/mutations/AppMutation';
|
||||
import { WorkflowRunMutation } from '../graphql/mutations/WorkflowRunMutation';
|
||||
import { AppQuery } from '../graphql/queries/AppQuery';
|
||||
import { WorkflowRunQuery } from '../graphql/queries/WorkflowRunQuery';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { ora } from '../ora';
|
||||
import { getPrivateExpoConfigAsync } from '../project/expoConfig';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { uploadAccountScopedFileAsync } from '../project/uploadAccountScopedFileAsync';
|
||||
import { uploadAccountScopedProjectSourceAsync } from '../project/uploadAccountScopedProjectSourceAsync';
|
||||
import { Actor, getActorDisplayName } from '../user/User';
|
||||
import { sleepAsync } from '../utils/promise';
|
||||
import { resolveVcsClient } from '../vcs';
|
||||
import { Client as VcsClient } from '../vcs/vcs';
|
||||
|
||||
// Expo Go release info - update when releasing a new version
|
||||
const EXPO_GO_SDK_VERSION = '55';
|
||||
const EXPO_GO_APP_VERSION = '55.0.11';
|
||||
const EXPO_GO_BUILD_NUMBER = '1017799';
|
||||
|
||||
const TESTFLIGHT_GROUP_NAME = 'Team (Expo)';
|
||||
|
||||
async function setupTestFlightAsync(ascApp: App): Promise<void> {
|
||||
// Create or get TestFlight group
|
||||
let group;
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
try {
|
||||
const groups = await ascApp.getBetaGroupsAsync({
|
||||
query: { includes: ['betaTesters'] },
|
||||
});
|
||||
|
||||
group = groups.find(
|
||||
g => g.attributes.isInternalGroup && g.attributes.name === TESTFLIGHT_GROUP_NAME
|
||||
);
|
||||
|
||||
if (!group) {
|
||||
group = await ascApp.createBetaGroupAsync({
|
||||
name: TESTFLIGHT_GROUP_NAME,
|
||||
isInternalGroup: true,
|
||||
hasAccessToAllBuilds: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
} catch (error: any) {
|
||||
// Apple returns this error when the app isn't ready yet
|
||||
if (error?.data?.errors?.some((e: any) => e.code === 'ENTITY_ERROR.RELATIONSHIP.INVALID')) {
|
||||
if (attempt < 9) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!group) {
|
||||
throw new Error('Failed to create TestFlight group');
|
||||
}
|
||||
|
||||
const users = await User.getAsync(ascApp.context);
|
||||
const admins = users.filter(u => u.attributes.roles?.includes(UserRole.ADMIN));
|
||||
|
||||
const existingEmails = new Set(
|
||||
group.attributes.betaTesters?.map((t: any) => t.attributes.email?.toLowerCase()) ?? []
|
||||
);
|
||||
|
||||
const newTesters = admins
|
||||
.filter(u => u.attributes.email && !existingEmails.has(u.attributes.email.toLowerCase()))
|
||||
.map(u => ({
|
||||
email: u.attributes.email!,
|
||||
firstName: u.attributes.firstName ?? '',
|
||||
lastName: u.attributes.lastName ?? '',
|
||||
}));
|
||||
|
||||
if (newTesters.length > 0) {
|
||||
await group.createBulkBetaTesterAssignmentsAsync(newTesters);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable no-console */
|
||||
async function withSuppressedOutputAsync<T>(fn: () => Promise<T>): Promise<T> {
|
||||
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
let capturedOutput = '';
|
||||
|
||||
const capture = (chunk: any): boolean => {
|
||||
if (typeof chunk === 'string') {
|
||||
capturedOutput += chunk;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Only suppress stdout, not stderr — ora writes spinner frames to stderr and
|
||||
// patching it would freeze the spinner animation during suppressed async work.
|
||||
process.stdout.write = capture as any;
|
||||
console.log = () => {};
|
||||
console.error = () => {};
|
||||
console.warn = () => {};
|
||||
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
process.stdout.write = originalStdoutWrite;
|
||||
console.log = originalConsoleLog;
|
||||
console.error = originalConsoleError;
|
||||
console.warn = originalConsoleWarn;
|
||||
|
||||
if (capturedOutput) {
|
||||
originalConsoleLog(capturedOutput);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
process.stdout.write = originalStdoutWrite;
|
||||
console.log = originalConsoleLog;
|
||||
console.error = originalConsoleError;
|
||||
console.warn = originalConsoleWarn;
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
|
||||
export default class Go extends EasCommand {
|
||||
static override description = 'Create a custom Expo Go and submit to TestFlight';
|
||||
static override hidden = true;
|
||||
|
||||
static override flags = {
|
||||
'bundle-id': Flags.string({
|
||||
description: 'iOS bundle identifier (auto-generated if not provided)',
|
||||
required: false,
|
||||
}),
|
||||
name: Flags.string({
|
||||
description: 'App name',
|
||||
default: 'My Expo Go',
|
||||
}),
|
||||
credentials: Flags.boolean({
|
||||
description: 'Interactively select credentials (default: auto-select)',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.log(
|
||||
chalk.bold(
|
||||
`Creating your personal Expo Go and deploying to TestFlight. ${learnMore('https://expo.fyi/deploy-expo-go-testflight')}`
|
||||
)
|
||||
);
|
||||
|
||||
const { flags } = await this.parse(Go);
|
||||
|
||||
const spinner = ora('Logging in to Expo...').start();
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
analytics,
|
||||
} = await this.getContextAsync(Go, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
spinner.succeed(`Logged in as ${chalk.cyan(getActorDisplayName(actor))}`);
|
||||
|
||||
const bundleId = flags['bundle-id'] ?? this.generateBundleId(actor);
|
||||
const appName = flags.name ?? 'My Expo Go';
|
||||
const slug = bundleId.split('.').pop() || 'my-expo-go';
|
||||
|
||||
const projectDir = path.join(os.tmpdir(), `eas-go-${slug}`);
|
||||
await fs.emptyDir(projectDir);
|
||||
|
||||
const originalCwd = process.cwd();
|
||||
process.chdir(projectDir);
|
||||
|
||||
const setupSpinner = ora('Creating project...').start();
|
||||
|
||||
// Step 1: Create project files and initialize git (silently)
|
||||
try {
|
||||
await withSuppressedOutputAsync(async () => {
|
||||
await this.createProjectFilesAsync(projectDir, bundleId, appName);
|
||||
await this.initGitRepoAsync(projectDir);
|
||||
});
|
||||
const vcsClient = resolveVcsClient();
|
||||
|
||||
// Step 2: Create/link EAS project (silently)
|
||||
const projectId = await withSuppressedOutputAsync(() =>
|
||||
this.ensureEasProjectAsync(graphqlClient, actor, projectDir, bundleId)
|
||||
);
|
||||
|
||||
// Step 3: Set up iOS credentials and create App Store Connect app
|
||||
const ascApp = await this.setupCredentialsAsync(
|
||||
projectDir,
|
||||
projectId,
|
||||
bundleId,
|
||||
appName,
|
||||
graphqlClient,
|
||||
actor,
|
||||
analytics,
|
||||
vcsClient,
|
||||
flags.credentials,
|
||||
() => {
|
||||
setupSpinner.stop();
|
||||
Log.markFreshLine();
|
||||
}
|
||||
);
|
||||
|
||||
// Step 4: Start workflow and monitor progress
|
||||
const { workflowUrl, workflowRunId } = await this.runWorkflowAsync(
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
actor,
|
||||
vcsClient
|
||||
);
|
||||
Log.withTick(`Workflow started: ${chalk.cyan(workflowUrl)}`);
|
||||
|
||||
const status = await this.monitorWorkflowJobsAsync(graphqlClient, workflowRunId);
|
||||
if (status === WorkflowRunStatus.Failure) {
|
||||
throw new Error('Workflow failed');
|
||||
} else if (status === WorkflowRunStatus.Canceled) {
|
||||
throw new Error('Workflow was canceled');
|
||||
}
|
||||
|
||||
// Step 5: Set up TestFlight group (silently)
|
||||
try {
|
||||
await setupTestFlightAsync(ascApp);
|
||||
} catch {
|
||||
// Non-fatal: TestFlight group setup failure shouldn't block the user
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
Log.succeed(
|
||||
`Done! Your custom Expo Go has been submitted to TestFlight. ${learnMore(
|
||||
`https://appstoreconnect.apple.com/apps/${ascApp.id}/testflight`,
|
||||
{ learnMoreMessage: 'Open it on App Store Connect' }
|
||||
)}`
|
||||
);
|
||||
Log.log(
|
||||
`App Store processing may take several minutes to complete. ${learnMore(
|
||||
'https://expo.fyi/personal-expo-go',
|
||||
{ learnMoreMessage: 'Learn more about Expo Go on TestFlight' }
|
||||
)}`
|
||||
);
|
||||
|
||||
await fs.remove(projectDir);
|
||||
} catch (error) {
|
||||
Log.gray(`Project files preserved for debugging: ${projectDir}`);
|
||||
throw error;
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
}
|
||||
|
||||
private generateBundleId(actor: Actor): string {
|
||||
const username = actor.accounts[0].name;
|
||||
// Sanitize username for bundle ID: only alphanumeric and hyphens allowed
|
||||
const sanitizedUsername = username
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '')
|
||||
.replace(/^-+|-+$/g, ''); // trim leading/trailing hyphens
|
||||
// Deterministic bundle ID per user + SDK version (reuses same ASC app)
|
||||
return `com.${sanitizedUsername || 'app'}.expogo${EXPO_GO_SDK_VERSION}`;
|
||||
}
|
||||
|
||||
private async createProjectFilesAsync(
|
||||
projectDir: string,
|
||||
bundleId: string,
|
||||
appName: string
|
||||
): Promise<void> {
|
||||
const slug = bundleId.split('.').pop() || 'custom-expo-go';
|
||||
const extensionBundleId = `${bundleId}.ExpoNotificationServiceExtension`;
|
||||
|
||||
const appJson = {
|
||||
expo: {
|
||||
name: appName,
|
||||
slug,
|
||||
version: EXPO_GO_APP_VERSION,
|
||||
ios: {
|
||||
bundleIdentifier: bundleId,
|
||||
buildNumber: EXPO_GO_BUILD_NUMBER,
|
||||
config: {
|
||||
usesNonExemptEncryption: false,
|
||||
},
|
||||
},
|
||||
extra: {
|
||||
eas: {
|
||||
build: {
|
||||
experimental: {
|
||||
ios: {
|
||||
appExtensions: [
|
||||
{
|
||||
targetName: 'ExpoNotificationServiceExtension',
|
||||
bundleIdentifier: extensionBundleId,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const easJson = {
|
||||
cli: {
|
||||
version: '>= 5.0.0',
|
||||
},
|
||||
build: {
|
||||
production: {
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
production: {
|
||||
ios: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const packageJson = {
|
||||
name: slug,
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
expo: '~54.0.0',
|
||||
},
|
||||
};
|
||||
|
||||
await fs.writeJson(path.join(projectDir, 'app.json'), appJson, { spaces: 2 });
|
||||
await fs.writeJson(path.join(projectDir, 'eas.json'), easJson, { spaces: 2 });
|
||||
await fs.writeJson(path.join(projectDir, 'package.json'), packageJson, { spaces: 2 });
|
||||
|
||||
await spawnAsync('npm', ['install'], { cwd: projectDir });
|
||||
}
|
||||
|
||||
private async initGitRepoAsync(projectDir: string): Promise<void> {
|
||||
await spawnAsync('git', ['init'], { cwd: projectDir });
|
||||
await spawnAsync('git', ['add', '.'], { cwd: projectDir });
|
||||
await spawnAsync('git', ['commit', '-m', 'Initial commit'], { cwd: projectDir });
|
||||
}
|
||||
|
||||
private async ensureEasProjectAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
actor: Actor,
|
||||
projectDir: string,
|
||||
bundleId: string
|
||||
): Promise<string> {
|
||||
const slug = bundleId.split('.').pop() || 'custom-expo-go';
|
||||
const account = actor.accounts[0];
|
||||
|
||||
const existingProjectId = await findProjectIdByAccountNameAndSlugNullableAsync(
|
||||
graphqlClient,
|
||||
account.name,
|
||||
slug
|
||||
);
|
||||
|
||||
if (existingProjectId) {
|
||||
await saveProjectIdToAppConfigAsync(projectDir, existingProjectId);
|
||||
return existingProjectId;
|
||||
}
|
||||
|
||||
const projectId = await AppMutation.createAppAsync(graphqlClient, {
|
||||
accountId: account.id,
|
||||
projectName: slug,
|
||||
});
|
||||
await saveProjectIdToAppConfigAsync(projectDir, projectId);
|
||||
return projectId;
|
||||
}
|
||||
|
||||
private async setupCredentialsAsync(
|
||||
projectDir: string,
|
||||
projectId: string,
|
||||
bundleId: string,
|
||||
appName: string,
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
actor: Actor,
|
||||
analytics: Analytics,
|
||||
vcsClient: VcsClient,
|
||||
customizeCreds: boolean,
|
||||
onBeforeAppleAuth?: () => void
|
||||
): Promise<App> {
|
||||
const exp = await getPrivateExpoConfigAsync(projectDir);
|
||||
const extensionBundleId = `${bundleId}.ExpoNotificationServiceExtension`;
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
projectInfo: { exp, projectId },
|
||||
nonInteractive: false,
|
||||
autoAcceptCredentialReuse: !customizeCreds,
|
||||
projectDir,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
});
|
||||
|
||||
onBeforeAppleAuth?.();
|
||||
const userAuthCtx = await credentialsCtx.appStore.ensureUserAuthenticatedAsync();
|
||||
|
||||
const app = await getAppFromContextAsync(credentialsCtx);
|
||||
|
||||
const targets: Target[] = [
|
||||
{
|
||||
targetName: exp.slug,
|
||||
bundleIdentifier: bundleId,
|
||||
entitlements: {},
|
||||
},
|
||||
{
|
||||
targetName: 'ExpoNotificationServiceExtension',
|
||||
bundleIdentifier: extensionBundleId,
|
||||
parentBundleIdentifier: bundleId,
|
||||
entitlements: {},
|
||||
},
|
||||
];
|
||||
|
||||
const ascApp = await withSuppressedOutputAsync(async () => {
|
||||
await new SetUpBuildCredentials({
|
||||
app,
|
||||
targets,
|
||||
distribution: 'store',
|
||||
}).runAsync(credentialsCtx);
|
||||
|
||||
const appLookupParams = {
|
||||
...app,
|
||||
bundleIdentifier: bundleId,
|
||||
};
|
||||
await new SetUpAscApiKey(appLookupParams, AppStoreApiKeyPurpose.SUBMISSION_SERVICE).runAsync(
|
||||
credentialsCtx
|
||||
);
|
||||
|
||||
const ascAppResult = await ensureAppExistsAsync(userAuthCtx, {
|
||||
name: appName,
|
||||
bundleIdentifier: bundleId,
|
||||
});
|
||||
|
||||
const easJsonPath = path.join(projectDir, 'eas.json');
|
||||
const easJson = await fs.readJson(easJsonPath);
|
||||
easJson.submit = easJson.submit || {};
|
||||
easJson.submit.production = easJson.submit.production || {};
|
||||
easJson.submit.production.ios = easJson.submit.production.ios || {};
|
||||
easJson.submit.production.ios.ascAppId = ascAppResult.id;
|
||||
await fs.writeJson(easJsonPath, easJson, { spaces: 2 });
|
||||
|
||||
await spawnAsync('git', ['add', 'eas.json'], { cwd: projectDir });
|
||||
await spawnAsync('git', ['commit', '-m', 'Add ascAppId to eas.json'], { cwd: projectDir });
|
||||
|
||||
return ascAppResult;
|
||||
});
|
||||
|
||||
// Set up push notifications (outside suppressed block so prompts are visible)
|
||||
const appLookupParamsForPushKey = { ...app, bundleIdentifier: bundleId };
|
||||
const setupPushKeyAction = new SetUpPushKey(appLookupParamsForPushKey);
|
||||
const isPushKeySetup = await setupPushKeyAction.isPushKeySetupAsync(credentialsCtx);
|
||||
if (!isPushKeySetup) {
|
||||
if (customizeCreds) {
|
||||
const wantsPushNotifications = await confirmAsync({
|
||||
message: 'Would you like to set up Push Notifications for your app?',
|
||||
initial: true,
|
||||
});
|
||||
if (wantsPushNotifications) {
|
||||
await setupPushKeyAction.runAsync(credentialsCtx);
|
||||
}
|
||||
} else {
|
||||
await setupPushKeyAction.runAsync(credentialsCtx);
|
||||
}
|
||||
}
|
||||
|
||||
return ascApp;
|
||||
}
|
||||
|
||||
private async runWorkflowAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectDir: string,
|
||||
projectId: string,
|
||||
actor: Actor,
|
||||
vcsClient: VcsClient
|
||||
): Promise<{ workflowUrl: string; workflowRunId: string }> {
|
||||
const account = actor.accounts[0];
|
||||
|
||||
const { projectArchiveBucketKey, easJsonBucketKey, packageJsonBucketKey } =
|
||||
await withSuppressedOutputAsync(async () => {
|
||||
const { projectArchiveBucketKey } = await uploadAccountScopedProjectSourceAsync({
|
||||
graphqlClient,
|
||||
vcsClient,
|
||||
accountId: account.id,
|
||||
});
|
||||
|
||||
const { fileBucketKey: easJsonBucketKey } = await uploadAccountScopedFileAsync({
|
||||
graphqlClient,
|
||||
accountId: account.id,
|
||||
filePath: path.join(projectDir, 'eas.json'),
|
||||
maxSizeBytes: 1024 * 1024,
|
||||
});
|
||||
|
||||
const { fileBucketKey: packageJsonBucketKey } = await uploadAccountScopedFileAsync({
|
||||
graphqlClient,
|
||||
accountId: account.id,
|
||||
filePath: path.join(projectDir, 'package.json'),
|
||||
maxSizeBytes: 1024 * 1024,
|
||||
});
|
||||
|
||||
return { projectArchiveBucketKey, easJsonBucketKey, packageJsonBucketKey };
|
||||
});
|
||||
|
||||
const { id: workflowRunId } = await WorkflowRunMutation.createExpoGoRepackWorkflowRunAsync(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: projectId,
|
||||
projectSource: {
|
||||
type: WorkflowProjectSourceType.Gcs,
|
||||
projectArchiveBucketKey,
|
||||
easJsonBucketKey,
|
||||
packageJsonBucketKey,
|
||||
projectRootDirectory: '.',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const app = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
const workflowUrl = `https://expo.dev/accounts/${account.name}/projects/${app.slug}/workflows/${workflowRunId}`;
|
||||
|
||||
return { workflowUrl, workflowRunId };
|
||||
}
|
||||
|
||||
private async monitorWorkflowJobsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
workflowRunId: string
|
||||
): Promise<WorkflowRunStatus> {
|
||||
const EXPECTED_BUILD_DURATION_SECONDS = 5 * 60;
|
||||
const EXPECTED_SUBMIT_DURATION_SECONDS = 2 * 60;
|
||||
const buildStartTime = Date.now();
|
||||
let submitStartTime: number | null = null;
|
||||
|
||||
const buildSpinner = ora(
|
||||
this.formatSpinnerText('Building Expo Go', EXPECTED_BUILD_DURATION_SECONDS, buildStartTime)
|
||||
).start();
|
||||
let submitSpinner: ReturnType<typeof ora> | null = null;
|
||||
let buildCompleted = false;
|
||||
let failedFetchesCount = 0;
|
||||
|
||||
while (true) {
|
||||
if (!buildCompleted) {
|
||||
buildSpinner.text = this.formatSpinnerText(
|
||||
'Building Expo Go',
|
||||
EXPECTED_BUILD_DURATION_SECONDS,
|
||||
buildStartTime
|
||||
);
|
||||
}
|
||||
if (submitSpinner && submitStartTime) {
|
||||
submitSpinner.text = this.formatSpinnerText(
|
||||
'Submitting to TestFlight',
|
||||
EXPECTED_SUBMIT_DURATION_SECONDS,
|
||||
submitStartTime
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const workflowRun = await WorkflowRunQuery.withJobsByIdAsync(graphqlClient, workflowRunId, {
|
||||
useCache: false,
|
||||
});
|
||||
failedFetchesCount = 0;
|
||||
|
||||
const repackJob = workflowRun.jobs.find(j => j.name === 'Repack Expo Go');
|
||||
const submitJob = workflowRun.jobs.find(j => j.name === 'Submit to TestFlight');
|
||||
|
||||
if (!buildCompleted) {
|
||||
if (repackJob?.status === WorkflowJobStatus.Success) {
|
||||
buildSpinner.succeed('Built Expo Go');
|
||||
buildCompleted = true;
|
||||
} else if (
|
||||
repackJob?.status === WorkflowJobStatus.Failure ||
|
||||
repackJob?.status === WorkflowJobStatus.Canceled
|
||||
) {
|
||||
buildSpinner.fail('Build failed');
|
||||
return WorkflowRunStatus.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
if (buildCompleted && submitSpinner === null && submitJob) {
|
||||
submitStartTime = Date.now();
|
||||
submitSpinner = ora(
|
||||
this.formatSpinnerText(
|
||||
'Submitting to TestFlight',
|
||||
EXPECTED_SUBMIT_DURATION_SECONDS,
|
||||
submitStartTime
|
||||
)
|
||||
).start();
|
||||
}
|
||||
|
||||
if (workflowRun.status === WorkflowRunStatus.Success) {
|
||||
submitSpinner?.stop();
|
||||
return WorkflowRunStatus.Success;
|
||||
} else if (workflowRun.status === WorkflowRunStatus.Failure) {
|
||||
buildSpinner.stop();
|
||||
submitSpinner?.fail('Submission failed');
|
||||
return WorkflowRunStatus.Failure;
|
||||
} else if (workflowRun.status === WorkflowRunStatus.Canceled) {
|
||||
buildSpinner.stop();
|
||||
submitSpinner?.stop();
|
||||
return WorkflowRunStatus.Canceled;
|
||||
}
|
||||
} catch {
|
||||
failedFetchesCount++;
|
||||
if (failedFetchesCount > 6) {
|
||||
buildSpinner.fail();
|
||||
submitSpinner?.fail();
|
||||
throw new Error('Failed to fetch the workflow run status 6 times in a row');
|
||||
}
|
||||
}
|
||||
|
||||
await sleepAsync(10 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private formatSpinnerText(
|
||||
label: string,
|
||||
expectedDurationSeconds: number,
|
||||
startTime: number
|
||||
): string {
|
||||
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
|
||||
const remainingSeconds = Math.max(0, expectedDurationSeconds - elapsedSeconds);
|
||||
|
||||
if (remainingSeconds === 0) {
|
||||
return `${label} (almost done...)`;
|
||||
}
|
||||
|
||||
const minutes = Math.ceil(remainingSeconds / 60);
|
||||
const unit = minutes === 1 ? 'minute' : 'minutes';
|
||||
return `${label} (~${minutes} ${unit} remaining)`;
|
||||
}
|
||||
}
|
||||
@@ -143,6 +143,67 @@ describe(UpdatePublish.name, () => {
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('errors when SDK >= 55 and --environment is not provided', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
|
||||
mockTestProject({ expoConfig: { sdkVersion: '55.0.0' } });
|
||||
|
||||
await expect(new UpdatePublish(flags, commandOptions).run()).rejects.toThrow(
|
||||
'--environment flag is required for projects using Expo SDK 55 or greater'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not error when SDK >= 55 and --environment is provided', async () => {
|
||||
const flags = [
|
||||
'--non-interactive',
|
||||
'--branch=branch123',
|
||||
'--message=abc',
|
||||
'--environment=production',
|
||||
];
|
||||
|
||||
mockTestProject({ expoConfig: { sdkVersion: '55.0.0' } });
|
||||
const { platforms, runtimeVersion } = mockTestExport();
|
||||
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branch: {
|
||||
id: 'branch123',
|
||||
name: 'wat',
|
||||
},
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(PublishMutation.publishUpdateGroupAsync)
|
||||
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
|
||||
|
||||
await new UpdatePublish(flags, commandOptions).run();
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not error when SDK < 55 and --environment is not provided', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
|
||||
mockTestProject({ expoConfig: { sdkVersion: '54.0.0' } });
|
||||
const { platforms, runtimeVersion } = mockTestExport();
|
||||
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branch: {
|
||||
id: 'branch123',
|
||||
name: 'wat',
|
||||
},
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(PublishMutation.publishUpdateGroupAsync)
|
||||
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
|
||||
|
||||
await new UpdatePublish(flags, commandOptions).run();
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a new update with the public expo config', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ensureEASUpdateIsConfiguredAsync,
|
||||
ensureEASUpdateIsConfiguredInEasJsonAsync,
|
||||
} from '../../update/configure';
|
||||
import { assertEnvironmentFlagForSdk55OrGreater } from '../../update/utils';
|
||||
|
||||
export default class UpdateConfigure extends EasCommand {
|
||||
static override description = 'configure the project to support EAS Update';
|
||||
@@ -42,6 +43,11 @@ export default class UpdateConfigure extends EasCommand {
|
||||
withServerSideEnvironment: flags['environment'] ?? null,
|
||||
});
|
||||
|
||||
assertEnvironmentFlagForSdk55OrGreater({
|
||||
sdkVersion: exp.sdkVersion,
|
||||
environment: flags['environment'],
|
||||
});
|
||||
|
||||
Log.log(
|
||||
'💡 The following process will configure your project to use EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags, EasUpdateEnvironmentFlag } from '../../commandUtils/flags';
|
||||
import { assertEnvironmentFlagForSdk55OrGreater } from '../../update/utils';
|
||||
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import fetch from '../../fetch';
|
||||
import {
|
||||
@@ -249,6 +250,11 @@ export default class UpdatePublish extends EasCommand {
|
||||
projectDir,
|
||||
} = await getDynamicPublicProjectConfigAsync();
|
||||
|
||||
assertEnvironmentFlagForSdk55OrGreater({
|
||||
sdkVersion: expPossiblyWithoutEasUpdateConfigured.sdkVersion,
|
||||
environment,
|
||||
});
|
||||
|
||||
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
|
||||
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
|
||||
@@ -25,6 +25,7 @@ export class CredentialsContext {
|
||||
public readonly appStore = new AppStoreApi();
|
||||
public readonly ios = IosGraphqlClient;
|
||||
public readonly nonInteractive: boolean;
|
||||
public readonly autoAcceptCredentialReuse: boolean;
|
||||
public readonly freezeCredentials: boolean = false;
|
||||
public readonly projectDir: string;
|
||||
public readonly user: Actor;
|
||||
@@ -50,6 +51,7 @@ export class CredentialsContext {
|
||||
analytics: Analytics;
|
||||
vcsClient: Client;
|
||||
freezeCredentials?: boolean;
|
||||
autoAcceptCredentialReuse?: boolean;
|
||||
env?: Env;
|
||||
}
|
||||
) {
|
||||
@@ -60,6 +62,7 @@ export class CredentialsContext {
|
||||
this.analytics = options.analytics;
|
||||
this.vcsClient = options.vcsClient;
|
||||
this.nonInteractive = options.nonInteractive ?? false;
|
||||
this.autoAcceptCredentialReuse = options.autoAcceptCredentialReuse ?? false;
|
||||
this.projectInfo = options.projectInfo;
|
||||
this.freezeCredentials = options.freezeCredentials ?? false;
|
||||
this.usesBroadcastPushNotifications =
|
||||
|
||||
@@ -66,6 +66,11 @@ export async function provideOrGenerateAscApiKeyAsync(
|
||||
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
|
||||
}
|
||||
|
||||
// When auto-accepting credentials, always auto-generate without asking for user input
|
||||
if (ctx.autoAcceptCredentialReuse) {
|
||||
return await generateAscApiKeyAsync(ctx, purpose);
|
||||
}
|
||||
|
||||
const userProvided = await promptForAscApiKeyAsync(ctx);
|
||||
if (!userProvided) {
|
||||
return await generateAscApiKeyAsync(ctx, purpose);
|
||||
|
||||
@@ -64,7 +64,7 @@ export class CreateProvisioningProfile {
|
||||
private async maybeGetUserProvidedAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<ProvisioningProfile | null> {
|
||||
if (ctx.nonInteractive) {
|
||||
if (ctx.nonInteractive || ctx.autoAcceptCredentialReuse) {
|
||||
return null;
|
||||
}
|
||||
const userProvided = await askForUserProvidedAsync(provisioningProfileSchema);
|
||||
|
||||
@@ -162,7 +162,7 @@ Remember that Apple Distribution Certificates are not application specific!
|
||||
export async function provideOrGenerateDistributionCertificateAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<DistributionCertificate> {
|
||||
if (!ctx.nonInteractive) {
|
||||
if (!ctx.nonInteractive && !ctx.autoAcceptCredentialReuse) {
|
||||
const userProvided = await promptForDistCertAsync(ctx);
|
||||
if (userProvided) {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { pushKeySchema } from '../credentials';
|
||||
import { isPushKeyValidAndTrackedAsync } from '../validators/validatePushKey';
|
||||
|
||||
export async function provideOrGeneratePushKeyAsync(ctx: CredentialsContext): Promise<PushKey> {
|
||||
if (!ctx.nonInteractive) {
|
||||
if (!ctx.nonInteractive && !ctx.autoAcceptCredentialReuse) {
|
||||
const userProvided = await promptForPushKeyAsync(ctx);
|
||||
if (userProvided) {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
|
||||
@@ -65,6 +65,12 @@ export class SetUpAscApiKey {
|
||||
return await new AssignAscApiKey(this.app).runAsync(ctx, maybeAutoselectedKey, this.purpose);
|
||||
}
|
||||
|
||||
// When auto-accepting credentials and no valid key was found, generate a new one without prompting
|
||||
if (ctx.autoAcceptCredentialReuse) {
|
||||
const ascApiKey = await new CreateAscApiKey(this.app.account).runAsync(ctx, this.purpose);
|
||||
return await new AssignAscApiKey(this.app).runAsync(ctx, ascApiKey, this.purpose);
|
||||
}
|
||||
|
||||
const availableChoices =
|
||||
keysForAccount.length === 0
|
||||
? this.choices.filter(choice => choice.value !== SetupAscApiKeyChoice.USE_EXISTING)
|
||||
@@ -90,9 +96,11 @@ export class SetUpAscApiKey {
|
||||
return null;
|
||||
}
|
||||
const [autoselectedKey] = sortAscApiKeysByUpdatedAtDesc(validKeys);
|
||||
const useAutoselected = await confirmAsync({
|
||||
message: `Reuse this App Store Connect API Key?\n${formatAscApiKey(autoselectedKey)}`,
|
||||
});
|
||||
const useAutoselected =
|
||||
ctx.autoAcceptCredentialReuse ||
|
||||
(await confirmAsync({
|
||||
message: `Reuse this App Store Connect API Key?\n${formatAscApiKey(autoselectedKey)}`,
|
||||
}));
|
||||
|
||||
if (useAutoselected) {
|
||||
Log.log(`Using App Store Connect API Key with ID ${autoselectedKey.keyIdentifier}`);
|
||||
|
||||
@@ -117,11 +117,13 @@ export class SetUpDistributionCertificate {
|
||||
const validDistCerts = await this.getValidDistCertsAsync(ctx);
|
||||
const autoselectedDistCert = validDistCerts[0];
|
||||
|
||||
const useAutoselected = await confirmAsync({
|
||||
message: `Reuse this distribution certificate?\n${formatDistributionCertificate(
|
||||
autoselectedDistCert
|
||||
)}`,
|
||||
});
|
||||
const useAutoselected =
|
||||
ctx.autoAcceptCredentialReuse ||
|
||||
(await confirmAsync({
|
||||
message: `Reuse this distribution certificate?\n${formatDistributionCertificate(
|
||||
autoselectedDistCert
|
||||
)}`,
|
||||
}));
|
||||
|
||||
if (useAutoselected) {
|
||||
Log.log(
|
||||
|
||||
@@ -142,7 +142,7 @@ export class SetUpProvisioningProfile {
|
||||
}
|
||||
|
||||
const isNonInteractiveOrUserDidConfirmAsync = async (): Promise<boolean> => {
|
||||
if (ctx.nonInteractive) {
|
||||
if (ctx.nonInteractive || ctx.autoAcceptCredentialReuse) {
|
||||
return true;
|
||||
}
|
||||
return await confirmAsync({
|
||||
|
||||
@@ -63,12 +63,14 @@ export class SetUpPushKey {
|
||||
if (validPushKeys.length > 0) {
|
||||
const autoselectedPushKey = validPushKeys[0];
|
||||
|
||||
const useAutoselected = await confirmAsync({
|
||||
message: `Reuse this Push Key?\n${formatPushKey(
|
||||
autoselectedPushKey,
|
||||
validPushKeys.map(pushKey => pushKey.keyIdentifier)
|
||||
)}`,
|
||||
});
|
||||
const useAutoselected =
|
||||
ctx.autoAcceptCredentialReuse ||
|
||||
(await confirmAsync({
|
||||
message: `Reuse this Push Key?\n${formatPushKey(
|
||||
autoselectedPushKey,
|
||||
validPushKeys.map(pushKey => pushKey.keyIdentifier)
|
||||
)}`,
|
||||
}));
|
||||
|
||||
if (useAutoselected) {
|
||||
Log.log(`Using push key with ID ${autoselectedPushKey.keyIdentifier}`);
|
||||
@@ -77,6 +79,11 @@ export class SetUpPushKey {
|
||||
}
|
||||
}
|
||||
|
||||
// When auto-accepting and no valid push key was found, generate a new one without prompting
|
||||
if (ctx.autoAcceptCredentialReuse) {
|
||||
return await new CreatePushKey(this.app.account).runAsync(ctx);
|
||||
}
|
||||
|
||||
const { action } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'action',
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CreateWorkflowRunFromGitRefMutationVariables,
|
||||
CreateWorkflowRunMutation,
|
||||
CreateWorkflowRunMutationVariables,
|
||||
WorkflowProjectSourceInput,
|
||||
WorkflowRevisionInput,
|
||||
WorkflowRunInput,
|
||||
} from '../generated';
|
||||
@@ -103,6 +104,40 @@ export namespace WorkflowRunMutation {
|
||||
return { id: data.workflowRun.createWorkflowRunFromGitRef.id };
|
||||
}
|
||||
|
||||
export async function createExpoGoRepackWorkflowRunAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
projectSource,
|
||||
}: {
|
||||
appId: string;
|
||||
projectSource: WorkflowProjectSourceInput;
|
||||
}
|
||||
): Promise<{ id: string }> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<
|
||||
{ workflowRun: { createExpoGoRepackWorkflowRun: { id: string } } },
|
||||
{ appId: string; projectSource: WorkflowProjectSourceInput }
|
||||
>(
|
||||
/* eslint-disable graphql/template-strings */
|
||||
gql`
|
||||
mutation CreateExpoGoRepackWorkflowRun($appId: ID!, $projectSource: WorkflowProjectSourceInput!) {
|
||||
workflowRun {
|
||||
createExpoGoRepackWorkflowRun(appId: $appId, projectSource: $projectSource) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
/* eslint-enable graphql/template-strings */
|
||||
{ appId, projectSource }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return { id: data.workflowRun.createExpoGoRepackWorkflowRun.id };
|
||||
}
|
||||
|
||||
export async function cancelWorkflowRunAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
|
||||
@@ -26,6 +26,15 @@ export default class Log {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal that the cursor is already at the start of a fresh line (e.g. after
|
||||
* a spinner clears itself), so that the next `addNewLineIfNone()` call knows
|
||||
* it doesn't need to emit an extra blank line.
|
||||
*/
|
||||
public static markFreshLine(): void {
|
||||
Log.isLastLineNewLine = true;
|
||||
}
|
||||
|
||||
public static error(...args: any[]): void {
|
||||
Log.consoleLog(...Log.withTextColor(args, chalk.red));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Errors } from '@oclif/core';
|
||||
import { format } from '@expo/timeago.js';
|
||||
import chalk from 'chalk';
|
||||
import dateFormat from 'dateformat';
|
||||
import semver from 'semver';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
@@ -357,3 +359,20 @@ export const updatePublishPlatformToAppPlatform: Record<UpdatePublishPlatform, A
|
||||
android: AppPlatform.Android,
|
||||
ios: AppPlatform.Ios,
|
||||
};
|
||||
|
||||
export function assertEnvironmentFlagForSdk55OrGreater({
|
||||
sdkVersion,
|
||||
environment,
|
||||
}: {
|
||||
sdkVersion: string | undefined;
|
||||
environment: string | undefined;
|
||||
}): void {
|
||||
if (process.env.EAS_BUILD) {
|
||||
return;
|
||||
}
|
||||
if (sdkVersion && semver.gte(sdkVersion, '55.0.0') && !environment) {
|
||||
Errors.error('--environment flag is required for projects using Expo SDK 55 or greater', {
|
||||
exit: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,86 +1,24 @@
|
||||
import assert from 'assert';
|
||||
import openBrowserAsync from 'better-opn';
|
||||
import crypto from 'crypto';
|
||||
import http from 'http';
|
||||
import { Socket } from 'node:net';
|
||||
import querystring from 'querystring';
|
||||
|
||||
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../api';
|
||||
import fetch from '../fetch';
|
||||
import { getExpoWebsiteBaseUrl } from '../api';
|
||||
import Log from '../log';
|
||||
|
||||
const CLIENT_ID = 'eas-cli';
|
||||
|
||||
function generateCodeVerifier(): string {
|
||||
return crypto.randomBytes(32).toString('base64url');
|
||||
}
|
||||
|
||||
function generateCodeChallenge(codeVerifier: string): string {
|
||||
return crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
||||
}
|
||||
|
||||
function generateState(): string {
|
||||
return crypto.randomBytes(32).toString('base64url');
|
||||
}
|
||||
|
||||
async function exchangeCodeForSessionSecretAsync({
|
||||
code,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
}: {
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
redirectUri: string;
|
||||
}): Promise<string> {
|
||||
const tokenUrl = `${getExpoApiBaseUrl()}/v2/auth/token`;
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: redirectUri,
|
||||
code_verifier: codeVerifier,
|
||||
client_id: CLIENT_ID,
|
||||
}),
|
||||
});
|
||||
|
||||
const result: any = await response.json();
|
||||
const sessionSecret = result?.data?.session_secret;
|
||||
if (!sessionSecret) {
|
||||
throw new Error('Failed to obtain session secret from token exchange.');
|
||||
}
|
||||
return sessionSecret;
|
||||
}
|
||||
|
||||
export async function getSessionUsingBrowserAuthFlowAsync({ sso = false }): Promise<string> {
|
||||
const scheme = 'http';
|
||||
const hostname = 'localhost';
|
||||
const callbackPath = '/auth/callback';
|
||||
const path = '/auth/callback';
|
||||
|
||||
const expoWebsiteUrl = getExpoWebsiteBaseUrl();
|
||||
|
||||
const codeVerifier = generateCodeVerifier();
|
||||
const codeChallenge = generateCodeChallenge(codeVerifier);
|
||||
const state = generateState();
|
||||
|
||||
const buildRedirectUri = (port: number): string =>
|
||||
`${scheme}://${hostname}:${port}${callbackPath}`;
|
||||
|
||||
const buildExpoLoginUrl = (port: number, sso: boolean): string => {
|
||||
// Note: we avoid URLSearchParams here because better-opn calls encodeURI()
|
||||
// on the URL before passing it to AppleScript, which would double-encode
|
||||
// the percent-encoded values from URLSearchParams.toString().
|
||||
const params = [
|
||||
`client_id=${CLIENT_ID}`,
|
||||
`redirect_uri=${buildRedirectUri(port)}`,
|
||||
`response_type=code`,
|
||||
`code_challenge=${codeChallenge}`,
|
||||
`code_challenge_method=S256`,
|
||||
`state=${state}`,
|
||||
`confirm_account=true`,
|
||||
].join('&');
|
||||
const params = querystring.stringify({
|
||||
confirm_account: true,
|
||||
app_redirect_uri: `${scheme}://${hostname}:${port}${path}`,
|
||||
});
|
||||
return `${expoWebsiteUrl}${sso ? '/sso-login' : '/login'}?${params}`;
|
||||
};
|
||||
|
||||
@@ -101,39 +39,22 @@ export async function getSessionUsingBrowserAuthFlowAsync({ sso = false }): Prom
|
||||
}
|
||||
};
|
||||
|
||||
const handleRequestAsync = async (): Promise<void> => {
|
||||
try {
|
||||
if (!(request.method === 'GET' && request.url?.includes('/auth/callback'))) {
|
||||
throw new Error('Unexpected login response.');
|
||||
}
|
||||
const url = new URL(request.url, `http:${request.headers.host}`);
|
||||
const code = url.searchParams.get('code');
|
||||
const returnedState = url.searchParams.get('state');
|
||||
const sessionSecret = url.searchParams.get('session_secret');
|
||||
|
||||
if (!code) {
|
||||
throw new Error('Request missing code search parameter.');
|
||||
if (!sessionSecret) {
|
||||
throw new Error('Request missing session_secret search parameter.');
|
||||
}
|
||||
if (returnedState !== state) {
|
||||
throw new Error('State mismatch. Possible CSRF attack.');
|
||||
}
|
||||
|
||||
const address = server.address();
|
||||
assert(address !== null && typeof address === 'object');
|
||||
const redirectUri = buildRedirectUri(address.port);
|
||||
|
||||
const sessionSecret = await exchangeCodeForSessionSecretAsync({
|
||||
code,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
});
|
||||
|
||||
resolve(sessionSecret);
|
||||
redirectAndCleanup('success');
|
||||
};
|
||||
|
||||
handleRequestAsync().catch(error => {
|
||||
} catch (error) {
|
||||
redirectAndCleanup('error');
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli-local-build-plugin",
|
||||
"version": "18.0.2",
|
||||
"version": "18.0.6",
|
||||
"description": "Tool for running EAS compatible builds on a local machine.",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
@@ -26,7 +26,7 @@
|
||||
"clean": "rm -rf node_modules dist coverage *.tsbuildinfo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/build-tools": "18.0.2",
|
||||
"@expo/build-tools": "18.0.6",
|
||||
"@expo/eas-build-job": "18.0.2",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/worker",
|
||||
"version": "18.0.2",
|
||||
"version": "18.0.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "yarn watch",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@expo/build-tools": "18.0.2",
|
||||
"@expo/build-tools": "18.0.6",
|
||||
"@expo/config-plugins": "8.0.8",
|
||||
"@expo/config-types": "52.0.1",
|
||||
"@expo/downloader": "18.0.1",
|
||||
|
||||
@@ -12,9 +12,11 @@ ROOT_DIR="$( cd "$SCRIPTS_DIR"/.. && pwd )"
|
||||
# to @expo/eas-build-job dependency, so the update-local-plugin step is no longer needed.
|
||||
# $SCRIPTS_DIR/bin/run update-local-plugin
|
||||
|
||||
next_version_bump=$($SCRIPTS_DIR/bin/run next-version)
|
||||
next_version_bump=`(cd $SCRIPTS_DIR; node --no-warnings=ExperimentalWarning --loader ts-node/esm src/nextVersion.ts)`
|
||||
next_version=${1:-$next_version_bump}
|
||||
|
||||
echo "next_version = ${next_version}"
|
||||
|
||||
if [[ "$GITHUB_USER" == "Expo CI" && "$GITHUB_EMAIL" == "[email protected]" && "$INPUT_DRY_RUN" != "true" ]]; then
|
||||
echo "Releasing with version $next_version"
|
||||
lerna version --yes --exact "$next_version"
|
||||
|
||||
@@ -2020,7 +2020,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/build-tools@npm:18.0.2, @expo/build-tools@workspace:packages/build-tools":
|
||||
"@expo/build-tools@npm:18.0.6, @expo/build-tools@workspace:packages/build-tools":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/build-tools@workspace:packages/build-tools"
|
||||
dependencies:
|
||||
@@ -2055,6 +2055,7 @@ __metadata:
|
||||
bplist-parser: "npm:0.3.2"
|
||||
bunyan: "npm:^1.8.15"
|
||||
fast-glob: "npm:^3.3.2"
|
||||
fast-xml-parser: "npm:^4.4.1"
|
||||
fs-extra: "npm:^11.2.0"
|
||||
gql.tada: "npm:^1.8.13"
|
||||
jest: "npm:^29.7.0"
|
||||
@@ -2717,7 +2718,7 @@ __metadata:
|
||||
resolution: "@expo/worker@workspace:packages/worker"
|
||||
dependencies:
|
||||
"@babel/core": "npm:7.24.7"
|
||||
"@expo/build-tools": "npm:18.0.2"
|
||||
"@expo/build-tools": "npm:18.0.6"
|
||||
"@expo/config-plugins": "npm:8.0.8"
|
||||
"@expo/config-types": "npm:52.0.1"
|
||||
"@expo/downloader": "npm:18.0.1"
|
||||
@@ -10120,7 +10121,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli-local-build-plugin@workspace:packages/local-build-plugin"
|
||||
dependencies:
|
||||
"@expo/build-tools": "npm:18.0.2"
|
||||
"@expo/build-tools": "npm:18.0.6"
|
||||
"@expo/eas-build-job": "npm:18.0.2"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/spawn-async": "npm:^1.7.2"
|
||||
|
||||
Reference in new issue
Block a user