Skip to content

Commit

Permalink
[eas-build-job] Allow steps in build jobs (#493)
Browse files Browse the repository at this point in the history
# Why

For workflows it'll be much easier to let users define `steps` in the workflow file instead of having to do the custom build config and stuff.

# How

Added ability to pass `steps` in build jobs. Alongside, we need to send `workflowInteprolationContext`, so added that too.

# Test Plan

Tested manually by running both manual and GitHub workflow build jobs. Also ran a custom build and a regular build from `eas-cli`.
  • Loading branch information
sjchmiela authored Jan 17, 2025
1 parent 0d2aa5c commit 1df3293
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 127 deletions.
38 changes: 24 additions & 14 deletions packages/build-tools/src/builders/custom.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import assert from 'assert';
import path from 'path';

import { BuildJob, BuildPhase, BuildTrigger, Ios, Platform } from '@expo/eas-build-job';
import { BuildConfigParser, BuildStepGlobalContext, errors } from '@expo/steps';
import nullthrows from 'nullthrows';
import { BuildJob, BuildPhase, BuildTrigger, Ios, Platform } from '@expo/eas-build-job';
import { BuildConfigParser, BuildStepGlobalContext, StepsConfigParser, errors } from '@expo/steps';

import { Artifacts, BuildContext } from '../context';
import { prepareProjectSourcesAsync } from '../common/projectSources';
Expand All @@ -25,23 +26,32 @@ export async function runCustomBuildAsync(ctx: BuildContext<BuildJob>): Promise<
ctx.updateEnv(env);
customBuildCtx.updateEnv(ctx.env);
}
const relativeConfigPath = nullthrows(
ctx.job.customBuildConfig?.path,
'Custom build config must be defined for custom builds'
);
const configPath = path.join(
ctx.getReactNativeProjectDirectory(customBuildCtx.projectSourceDirectory),
relativeConfigPath

assert(
'steps' in ctx.job || 'customBuildConfig' in ctx.job,
'Steps or custom build config path are required in custom jobs'
);

const globalContext = new BuildStepGlobalContext(customBuildCtx, false);
const easFunctions = getEasFunctions(customBuildCtx);
const easFunctionGroups = getEasFunctionGroups(customBuildCtx);
const parser = new BuildConfigParser(globalContext, {
externalFunctions: easFunctions,
externalFunctionGroups: easFunctionGroups,
configPath,
});
const parser = ctx.job.steps
? new StepsConfigParser(globalContext, {
externalFunctions: easFunctions,
externalFunctionGroups: easFunctionGroups,
steps: ctx.job.steps,
})
: new BuildConfigParser(globalContext, {
externalFunctions: easFunctions,
externalFunctionGroups: easFunctionGroups,
configPath: path.join(
ctx.getReactNativeProjectDirectory(customBuildCtx.projectSourceDirectory),
nullthrows(
ctx.job.customBuildConfig?.path,
'Steps or custom build config path are required in custom jobs'
)
),
});
const workflow = await ctx.runBuildPhase(BuildPhase.PARSE_CUSTOM_WORKFLOW_CONFIG, async () => {
try {
return await parser.parseAsync();
Expand Down
27 changes: 6 additions & 21 deletions packages/build-tools/src/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@ import fs from 'fs';
import path from 'path';

import { BuildPhase, Generic } from '@expo/eas-build-job';
import {
BuildConfigParser,
BuildStepGlobalContext,
BuildWorkflow,
errors,
StepsConfigParser,
} from '@expo/steps';
import { BuildStepGlobalContext, BuildWorkflow, errors, StepsConfigParser } from '@expo/steps';
import { Result, asyncResult } from '@expo/results';

import { BuildContext } from './context';
Expand All @@ -30,20 +24,11 @@ export async function runGenericJobAsync(

const globalContext = new BuildStepGlobalContext(customBuildCtx, false);

const parser = ctx.job.steps
? new StepsConfigParser(globalContext, {
externalFunctions: getEasFunctions(customBuildCtx),
externalFunctionGroups: getEasFunctionGroups(customBuildCtx),
steps: ctx.job.steps,
})
: new BuildConfigParser(globalContext, {
externalFunctions: getEasFunctions(customBuildCtx),
externalFunctionGroups: getEasFunctionGroups(customBuildCtx),
configPath: path.join(
customBuildCtx.projectSourceDirectory,
ctx.job.customBuildConfig.path
),
});
const parser = new StepsConfigParser(globalContext, {
externalFunctions: getEasFunctions(customBuildCtx),
externalFunctionGroups: getEasFunctionGroups(customBuildCtx),
steps: ctx.job.steps,
});

const workflow = await ctx.runBuildPhase(BuildPhase.PARSE_CUSTOM_WORKFLOW_CONFIG, async () => {
try {
Expand Down
52 changes: 51 additions & 1 deletion packages/eas-build-job/src/__tests__/android.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ describe('Android.JobSchema', () => {
expect(error?.message).toBe('"buildProfile" is required');
});

test('valid custom build job', () => {
test('valid custom build job with path', () => {
const customBuildJob = {
mode: BuildMode.CUSTOM,
type: Workflow.UNKNOWN,
Expand All @@ -236,6 +236,56 @@ describe('Android.JobSchema', () => {
expect(error).toBeFalsy();
});

test('valid custom build job with steps', () => {
const customBuildJob = {
mode: BuildMode.CUSTOM,
type: Workflow.UNKNOWN,
platform: Platform.ANDROID,
projectArchive: {
type: ArchiveSourceType.URL,
url: 'https://expo.dev/builds/123',
},
projectRootDirectory: '.',
steps: [
{
id: 'step1',
name: 'Step 1',
run: 'echo Hello, world!',
shell: 'sh',
},
],
outputs: {},
initiatingUserId: randomUUID(),
appId: randomUUID(),
workflowInterpolationContext: {
after: {
setup: {
status: 'success',
outputs: {},
},
},
needs: {
setup: {
status: 'success',
outputs: {},
},
},
github: {
event_name: 'push',
sha: '123',
ref: 'master',
ref_name: 'master',
ref_type: 'branch',
},
env: { EXPO_TOKEN: randomUUID() },
},
};

const { value, error } = Android.JobSchema.validate(customBuildJob, joiOptions);
expect(value).toMatchObject(customBuildJob);
expect(error).toBeFalsy();
});

test('can set github trigger options', () => {
const job = {
mode: BuildMode.BUILD,
Expand Down
67 changes: 12 additions & 55 deletions packages/eas-build-job/src/__tests__/generic.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { randomUUID } from 'crypto';

import { ZodError } from 'zod';

import { ArchiveSourceType, BuildTrigger, EnvironmentSecretType } from '../common';
import { Generic } from '../generic';

Expand All @@ -12,9 +14,14 @@ describe('Generic.JobZ', () => {
gitCommitHash: '1234567890',
gitRef: null,
},
customBuildConfig: {
path: 'path/to/custom-build-config.yml',
},
steps: [
{
id: 'step1',
name: 'Step 1',
run: 'echo Hello, world!',
shell: 'sh',
},
],
secrets: {
robotAccessToken: 'token',
environmentSecrets: [
Expand Down Expand Up @@ -84,7 +91,7 @@ describe('Generic.JobZ', () => {
expect(Generic.JobZ.parse(job)).toEqual(job);
});

it('errors when neither customBuildConfig.path nor steps are provided', () => {
it('errors when steps are not provided', () => {
const job: Omit<Generic.Job, 'customBuildConfig' | 'steps'> = {
projectArchive: {
type: ArchiveSourceType.GIT,
Expand Down Expand Up @@ -114,56 +121,6 @@ describe('Generic.JobZ', () => {
appId: randomUUID(),
initiatingUserId: randomUUID(),
};
expect(() => Generic.JobZ.parse(job)).toThrow('Invalid input');
});

it('errors when both customBuildConfig.path and steps are provided', () => {
const job: Omit<Generic.Job, 'customBuildConfig' | 'steps'> & {
customBuildConfig: NonNullable<Generic.Job['customBuildConfig']>;
steps: NonNullable<Generic.Job['steps']>;
} = {
projectArchive: {
type: ArchiveSourceType.GIT,
repositoryUrl: 'https://github.com/expo/expo.git',
gitCommitHash: '1234567890',
gitRef: null,
},
secrets: {
robotAccessToken: 'token',
environmentSecrets: [
{
name: 'secret-name',
value: 'secret-value',
type: EnvironmentSecretType.STRING,
},
],
},
expoDevUrl: 'https://expo.dev/accounts/name/builds/id',
builderEnvironment: {
image: 'macos-sonoma-14.5-xcode-15.4',
node: '20.15.1',
env: {
KEY1: 'value1',
},
},
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
customBuildConfig: {
path: 'path/to/custom-build-config.yml',
},
steps: [
{
id: 'step1',
name: 'Step 1',
run: 'echo Hello, world!',
shell: 'sh',
env: {
KEY1: 'value1',
},
},
],
appId: randomUUID(),
initiatingUserId: randomUUID(),
};
expect(() => Generic.JobZ.parse(job)).toThrow('Invalid input');
expect(() => Generic.JobZ.parse(job)).toThrow(ZodError);
});
});
50 changes: 50 additions & 0 deletions packages/eas-build-job/src/__tests__/ios.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,56 @@ describe('Ios.JobSchema', () => {
expect(error).toBeFalsy();
});

test('valid custom build job with steps', () => {
const customBuildJob = {
mode: BuildMode.CUSTOM,
type: Workflow.UNKNOWN,
platform: Platform.IOS,
projectArchive: {
type: ArchiveSourceType.URL,
url: 'https://expo.dev/builds/123',
},
projectRootDirectory: '.',
steps: [
{
id: 'step1',
name: 'Step 1',
run: 'echo Hello, world!',
shell: 'sh',
},
],
outputs: {},
initiatingUserId: randomUUID(),
appId: randomUUID(),
workflowInterpolationContext: {
after: {
setup: {
status: 'success',
outputs: {},
},
},
needs: {
setup: {
status: 'success',
outputs: {},
},
},
github: {
event_name: 'push',
sha: '123',
ref: 'master',
ref_name: 'master',
ref_type: 'branch',
},
env: { EXPO_TOKEN: randomUUID() },
},
};

const { value, error } = Ios.JobSchema.validate(customBuildJob, joiOptions);
expect(value).toMatchObject(customBuildJob);
expect(error).toBeFalsy();
});

test('invalid generic job', () => {
const genericJob = {
secrets: {
Expand Down
22 changes: 12 additions & 10 deletions packages/eas-build-job/src/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import {
EnvironmentSecret,
BuildTrigger,
BuildMode,
StaticWorkflowInterpolationContextZ,
StaticWorkflowInterpolationContext,
CustomBuildConfigSchema,
} from './common';
import { Step } from './step';

export interface Keystore {
dataBase64: string;
Expand Down Expand Up @@ -100,6 +104,8 @@ export interface Job {
customBuildConfig?: {
path: string;
};
steps?: Step[];
outputs?: Record<string, string>;

experimental?: {
prebuildCommand?: string;
Expand All @@ -111,7 +117,7 @@ export interface Job {
};
loggerLevel?: LoggerLevel;

workflowInterpolationContext?: never;
workflowInterpolationContext?: StaticWorkflowInterpolationContext;

initiatingUserId: string;
appId: string;
Expand Down Expand Up @@ -165,14 +171,6 @@ export const JobSchema = Joi.object({
buildType: Joi.string().valid(...Object.values(BuildType)),
username: Joi.string(),

customBuildConfig: Joi.when('mode', {
is: Joi.string().valid(BuildMode.CUSTOM),
then: Joi.object({
path: Joi.string(),
}).required(),
otherwise: Joi.any().strip(),
}),

experimental: Joi.object({
prebuildCommand: Joi.string(),
}),
Expand All @@ -187,4 +185,8 @@ export const JobSchema = Joi.object({
appId: Joi.string().required(),

environment: Joi.string().valid('production', 'preview', 'development'),
});

workflowInterpolationContext: Joi.object().custom((workflowInterpolationContext) =>
StaticWorkflowInterpolationContextZ.optional().parse(workflowInterpolationContext)
),
}).concat(CustomBuildConfigSchema);
Loading

0 comments on commit 1df3293

Please sign in to comment.