Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This is the log of notable changes to EAS CLI and related packages.
- [build-tools] Stream iOS Simulator previews at 1600px and 10 Mbps. ([#4439](https://github.com/expo/eas-cli/pull/4439) by [@gwdp](https://github.com/gwdp))
- [eas-build-job] Add the `DeviceRunSession.Job` payload for EAS Simulator sessions. ([#4412](https://github.com/expo/eas-cli/pull/4412) by [@szdziedzic](https://github.com/szdziedzic))
- [build-tools] Add a device run session runner that plans EAS Simulator sessions as a task graph, so device-independent work overlaps the boot. ([#4413](https://github.com/expo/eas-cli/pull/4413) by [@szdziedzic](https://github.com/szdziedzic))
- [worker] Run `DeviceRunSession.Job` payloads with the device run session runner. ([#4414](https://github.com/expo/eas-cli/pull/4414) by [@szdziedzic](https://github.com/szdziedzic))

### 馃悰 Bug fixes

Expand Down
123 changes: 123 additions & 0 deletions packages/worker/src/__unit__/build.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Builders, runDeviceRunSessionJobAsync, runGenericJobAsync } from '@expo/build-tools';
import {
ArchiveSourceType,
BuildTrigger,
DeviceRunSession,
Job,
Platform,
} from '@expo/eas-build-job';

import { build } from '../build';

jest.mock('@expo/build-tools', () => {
const actual = jest.requireActual('@expo/build-tools');
return {
...actual,
Builders: { androidBuilder: jest.fn(), iosBuilder: jest.fn() },
TurtleSshSession: { isSshEnabled: () => false },
runGenericJobAsync: jest.fn(),
runDeviceRunSessionJobAsync: jest.fn(),
};
});
jest.mock('../runtimeEnvironment', () => ({ prepareRuntimeEnvironment: jest.fn() }));
jest.mock('../displayRuntimeInfo', () => ({ displayWorkerRuntimeInfo: jest.fn() }));
jest.mock('../workingdir', () => ({ cleanUpWorkingdir: jest.fn() }));
jest.mock('../config', () => {
const actual = jest.requireActual('../config').default;
return { __esModule: true, default: { ...actual, env: 'production' } };
});
jest.mock('../logger', () => ({
__esModule: true,
default: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() },
}));

const mockedRunDeviceRunSessionJobAsync = jest.mocked(runDeviceRunSessionJobAsync);
const mockedRunGenericJobAsync = jest.mocked(runGenericJobAsync);
const mockedBuilders = jest.mocked(Builders);

const sharedJobFields: Omit<DeviceRunSession.Job, 'type' | 'session' | 'device'> = {
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
projectArchive: { type: ArchiveSourceType.NONE },
secrets: { robotAccessToken: 'token', environmentSecrets: [] },
expoDevUrl: 'https://expo.dev/',
builderEnvironment: { image: 'latest', env: {} },
initiatingUserId: 'user-id',
appId: 'app-id',
};

const deviceRunSessionJob: DeviceRunSession.Job = {
...sharedJobFields,
type: DeviceRunSession.JobType.DEVICE_RUN_SESSION,
session: {
id: 'session-id',
controller: DeviceRunSession.Controller.WEB_PREVIEW_ONLY,
maxDurationSeconds: 600,
ngrokTunnelDomain: 'sim.example.test',
},
device: { platform: Platform.IOS },
};

const genericJob: Job = {
...sharedJobFields,
steps: [{ id: 'step', name: 'Step', run: 'true', shell: 'sh' }],
};

function createContext(job: Job): any {
const logger: Record<string, jest.Mock> = {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
logger.child = jest.fn(() => logger);
return {
job,
logger,
runBuildPhase: jest.fn(async (_phase: unknown, fn: () => Promise<unknown>) => await fn()),
};
}

function createAnalytics(): any {
return { logEvent: jest.fn(), flushEventsAsync: jest.fn(async () => {}) };
}

describe(build, () => {
beforeEach(() => {
jest.clearAllMocks();
mockedRunDeviceRunSessionJobAsync.mockResolvedValue(undefined);
mockedRunGenericJobAsync.mockResolvedValue({
runResult: { ok: true } as never,
buildWorkflow: {} as never,
});
});

it('runs device run session jobs with the dedicated runner', async () => {
const ctx = createContext(deviceRunSessionJob);

const artifacts = await build({ ctx, buildId: 'job-run-id', analytics: createAnalytics() });

expect(artifacts).toEqual({});
expect(mockedRunDeviceRunSessionJobAsync).toHaveBeenCalledWith(ctx);
expect(mockedRunGenericJobAsync).not.toHaveBeenCalled();
expect(mockedBuilders.iosBuilder).not.toHaveBeenCalled();
});

it('still runs generic jobs with the steps runner', async () => {
const ctx = createContext(genericJob);

await build({ ctx, buildId: 'job-run-id', analytics: createAnalytics() });

expect(mockedRunGenericJobAsync).toHaveBeenCalledWith(ctx);
expect(mockedRunDeviceRunSessionJobAsync).not.toHaveBeenCalled();
});

it('propagates the runner error so the job run is reported as errored', async () => {
const error = new Error('simulator did not boot');
mockedRunDeviceRunSessionJobAsync.mockRejectedValue(error);
const analytics = createAnalytics();

await expect(
build({ ctx: createContext(deviceRunSessionJob), buildId: 'job-run-id', analytics })
).rejects.toBe(error);
expect(analytics.flushEventsAsync).toHaveBeenCalled();
});
});
8 changes: 8 additions & 0 deletions packages/worker/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
BuildContext,
Builders,
TurtleSshSession,
runDeviceRunSessionJobAsync,
runGenericJobAsync,
} from '@expo/build-tools';
import {
Expand All @@ -11,6 +12,7 @@ import {
BuildMode,
BuildPhase,
BuildPhaseResult,
DeviceRunSession,
Generic,
Ios,
LogMarker,
Expand Down Expand Up @@ -80,6 +82,12 @@ export async function build({
}
case undefined: {
artifacts = {};
if (job.type === DeviceRunSession.JobType.DEVICE_RUN_SESSION) {
// EAS Simulator sessions: the runner owns the lifecycle, validates the
// payload, and rejects with the error to report on the job run.
await runDeviceRunSessionJobAsync(ctx as BuildContext<DeviceRunSession.Job>);
break;
}
const buildCtx = ctx as BuildContext<Generic.Job>;
const { runResult } = await runGenericJobAsync(buildCtx);

Expand Down
4 changes: 2 additions & 2 deletions packages/worker/src/external/turtle.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type SignedUrl } from '@expo/build-tools';
import { BuildJob, Generic, Metadata, errors } from '@expo/eas-build-job';
import { BuildJob, DeviceRunSession, Generic, Metadata, errors } from '@expo/eas-build-job';

export const androidImagesWithJavaVersionLowerThen11 = [
'ubuntu-20.04-jdk-8-ndk-r19c',
Expand Down Expand Up @@ -113,7 +113,7 @@ export namespace LauncherMessage {
}
| {
jobType: 'jobRun';
job: Generic.Job;
job: Generic.Job | DeviceRunSession.Job;
jobRunId: string;
}
);
Expand Down
3 changes: 2 additions & 1 deletion packages/worker/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,8 @@ export default class BuildService {
tags: {
...(err.buildPhase ? { buildPhase: err.buildPhase } : {}),
errorCode: err.trackingCode ?? err.errorCode,
...('type' in job ? { workflow: job.type } : {}),
// `type` is the workflow on build jobs and the job family on session jobs.
...(job.platform ? { workflow: job.type } : job.type ? { jobType: job.type } : {}),
},
extras: {
buildId: this.buildId,
Expand Down
Loading