From 0672009d403f43696e1693b964ef567276f37478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Tue, 17 Feb 2026 21:58:05 +0100 Subject: [PATCH 1/2] Add hooks map to build jobs --- .../src/__tests__/android.test.ts | 54 ++++++++++ .../src/__tests__/generic.test.ts | 101 ++++++++++++++++++ .../eas-build-job/src/__tests__/ios.test.ts | 60 +++++++++++ packages/eas-build-job/src/android.ts | 4 + packages/eas-build-job/src/common.ts | 20 +++- packages/eas-build-job/src/generic.ts | 2 + packages/eas-build-job/src/index.ts | 1 + packages/eas-build-job/src/ios.ts | 4 + 8 files changed, 245 insertions(+), 1 deletion(-) diff --git a/packages/eas-build-job/src/__tests__/android.test.ts b/packages/eas-build-job/src/__tests__/android.test.ts index 4705e7e773..1c1412f537 100644 --- a/packages/eas-build-job/src/__tests__/android.test.ts +++ b/packages/eas-build-job/src/__tests__/android.test.ts @@ -340,6 +340,60 @@ describe('Android.JobSchema', () => { expect(error).toBeFalsy(); }); + test('can set hooks with arbitrary names', () => { + const job = { + mode: BuildMode.BUILD, + type: Workflow.GENERIC, + platform: Platform.ANDROID, + projectArchive: { + type: ArchiveSourceType.URL, + url: 'https://expo.dev/builds/123', + }, + projectRootDirectory: '.', + hooks: { + before_install_node_modules: [ + { + id: 'before-install', + run: 'echo before install', + shell: 'sh', + }, + ], + my_custom_hook: [], + }, + secrets, + initiatingUserId: randomUUID(), + appId: randomUUID(), + }; + const { value, error } = Android.JobSchema.validate(job, joiOptions); + expect(value).toMatchObject(job); + expect(error).toBeFalsy(); + }); + + test('can set hooks in custom mode', () => { + const customBuildJob = { + mode: BuildMode.CUSTOM, + type: Workflow.UNKNOWN, + platform: Platform.ANDROID, + projectArchive: { + type: ArchiveSourceType.URL, + url: 'https://expo.dev/builds/123', + }, + projectRootDirectory: '.', + customBuildConfig: { + path: 'production.android.yml', + }, + hooks: { + before_install_node_modules: [], + }, + initiatingUserId: randomUUID(), + appId: 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, diff --git a/packages/eas-build-job/src/__tests__/generic.test.ts b/packages/eas-build-job/src/__tests__/generic.test.ts index 14548d0b96..c330a351c3 100644 --- a/packages/eas-build-job/src/__tests__/generic.test.ts +++ b/packages/eas-build-job/src/__tests__/generic.test.ts @@ -92,6 +92,107 @@ describe('Generic.JobZ', () => { expect(Generic.JobZ.parse(job)).toEqual(job); }); + it('accepts hooks with arbitrary names', () => { + const job: Generic.Job = { + projectArchive: { + type: ArchiveSourceType.GIT, + repositoryUrl: 'https://github.com/expo/expo.git', + gitCommitHash: '1234567890', + gitRef: null, + }, + hooks: { + before_install_node_modules: [ + { + id: 'before-install', + run: 'echo before install', + shell: 'sh', + }, + ], + my_custom_hook: [], + }, + steps: [ + { + id: 'step1', + name: 'Step 1', + run: 'echo Hello, world!', + shell: 'sh', + }, + ], + 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', + corepack: false, + env: { + KEY1: 'value1', + }, + }, + triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION, + appId: randomUUID(), + initiatingUserId: randomUUID(), + }; + expect(Generic.JobZ.parse(job)).toEqual(job); + }); + + it('errors when hook step definition is invalid', () => { + const job = { + projectArchive: { + type: ArchiveSourceType.GIT, + repositoryUrl: 'https://github.com/expo/expo.git', + gitCommitHash: '1234567890', + gitRef: null, + }, + hooks: { + before_install_node_modules: [ + { + id: 'missing-run-or-uses', + }, + ], + }, + steps: [ + { + id: 'step1', + name: 'Step 1', + run: 'echo Hello, world!', + shell: 'sh', + }, + ], + 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', + corepack: false, + env: { + KEY1: 'value1', + }, + }, + triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION, + appId: randomUUID(), + initiatingUserId: randomUUID(), + }; + expect(() => Generic.JobZ.parse(job)).toThrow(ZodError); + }); + it('errors when steps are not provided', () => { const job: Omit = { projectArchive: { diff --git a/packages/eas-build-job/src/__tests__/ios.test.ts b/packages/eas-build-job/src/__tests__/ios.test.ts index 3574cd7fcd..f25e426c15 100644 --- a/packages/eas-build-job/src/__tests__/ios.test.ts +++ b/packages/eas-build-job/src/__tests__/ios.test.ts @@ -84,6 +84,66 @@ describe('Ios.JobSchema', () => { expect(error).toBeFalsy(); }); + test('valid generic job with hooks', () => { + const genericJob = { + secrets: { + buildCredentials, + }, + type: Workflow.GENERIC, + platform: Platform.IOS, + projectArchive: { + type: ArchiveSourceType.URL, + url: 'http://localhost:3000', + }, + projectRootDirectory: '.', + hooks: { + before_install_node_modules: [ + { + id: 'before-install', + run: 'echo before install', + shell: 'sh', + }, + ], + my_custom_hook: [], + }, + initiatingUserId: randomUUID(), + appId: randomUUID(), + }; + + const { value, error } = Ios.JobSchema.validate(genericJob, joiOptions); + expect(value).toMatchObject(genericJob); + expect(error).toBeFalsy(); + }); + + test('valid resign job with hooks', () => { + const genericJob = { + mode: BuildMode.RESIGN, + secrets: { + buildCredentials, + }, + type: Workflow.UNKNOWN, + platform: Platform.IOS, + projectArchive: { + type: ArchiveSourceType.NONE, + }, + resign: { + applicationArchiveSource: { + type: ArchiveSourceType.URL, + url: 'http://localhost:3000/a.ipa', + }, + }, + hooks: { + before_install_node_modules: [], + }, + initiatingUserId: randomUUID(), + appId: randomUUID(), + }; + + const { value, error } = Ios.JobSchema.validate(genericJob, joiOptions); + expect(value).toMatchObject(genericJob); + expect(error).toBeFalsy(); + }); + test('valid custom build job with metadataLocation', () => { const customBuildJob = { mode: BuildMode.CUSTOM, diff --git a/packages/eas-build-job/src/android.ts b/packages/eas-build-job/src/android.ts index 3bf047984e..0ece6b3409 100644 --- a/packages/eas-build-job/src/android.ts +++ b/packages/eas-build-job/src/android.ts @@ -13,6 +13,8 @@ import { EnvSchema, EnvironmentSecret, EnvironmentSecretsSchema, + Hooks, + HooksSchema, Platform, StaticWorkflowInterpolationContext, StaticWorkflowInterpolationContextZ, @@ -106,6 +108,7 @@ export interface Job { customBuildConfig?: { path: string; }; + hooks?: Hooks; steps?: Step[]; outputs?: Record; @@ -172,6 +175,7 @@ export const JobSchema = Joi.object({ buildType: Joi.string().valid(...Object.values(BuildType)), username: Joi.string(), + hooks: HooksSchema, experimental: Joi.object({ prebuildCommand: Joi.string(), diff --git a/packages/eas-build-job/src/common.ts b/packages/eas-build-job/src/common.ts index 94db5f5b82..7604aa23f4 100644 --- a/packages/eas-build-job/src/common.ts +++ b/packages/eas-build-job/src/common.ts @@ -2,7 +2,7 @@ import Joi from 'joi'; import { z } from 'zod'; import { BuildPhase, BuildPhaseResult } from './logs'; -import { validateSteps } from './step'; +import { Step, StepZ, validateSteps } from './step'; export enum BuildMode { BUILD = 'build', @@ -142,6 +142,24 @@ export const EnvironmentSecretZ = z.object({ type: z.nativeEnum(EnvironmentSecretType), }); +export type Hooks = Record; +export const HooksSchema = Joi.object().pattern( + Joi.string(), + Joi.array() + .items(Joi.any()) + .required() + .custom( + steps => { + if (steps.length === 0) { + return steps; + } + return validateSteps(steps); + }, + 'steps validation' + ) +); +export const HooksZ = z.record(z.string(), z.array(StepZ)); + export interface Cache { disabled: boolean; clear: boolean; diff --git a/packages/eas-build-job/src/generic.ts b/packages/eas-build-job/src/generic.ts index 14449bc523..bca485fd6c 100644 --- a/packages/eas-build-job/src/generic.ts +++ b/packages/eas-build-job/src/generic.ts @@ -5,6 +5,7 @@ import { ArchiveSourceSchemaZ, BuildTrigger, EnvironmentSecretZ, + HooksZ, StaticWorkflowInterpolationContextZ, } from './common'; import { StepZ } from './step'; @@ -45,6 +46,7 @@ export namespace Generic { initiatingUserId: z.string(), appId: z.string(), + hooks: HooksZ.optional(), steps: z.array(StepZ).min(1), outputs: z.record(z.string(), z.string()).optional(), }); diff --git a/packages/eas-build-job/src/index.ts b/packages/eas-build-job/src/index.ts index 1cd45645a0..ad315c2a02 100644 --- a/packages/eas-build-job/src/index.ts +++ b/packages/eas-build-job/src/index.ts @@ -11,6 +11,7 @@ export { Env, EnvironmentSecret, EnvironmentSecretType, + Hooks, Workflow, Platform, Cache, diff --git a/packages/eas-build-job/src/ios.ts b/packages/eas-build-job/src/ios.ts index 40260e9a65..d220b45e9a 100644 --- a/packages/eas-build-job/src/ios.ts +++ b/packages/eas-build-job/src/ios.ts @@ -13,6 +13,8 @@ import { EnvSchema, EnvironmentSecret, EnvironmentSecretsSchema, + Hooks, + HooksSchema, Platform, StaticWorkflowInterpolationContext, StaticWorkflowInterpolationContextZ, @@ -120,6 +122,7 @@ export interface Job { customBuildConfig?: { path: string; }; + hooks?: Hooks; steps?: Step[]; outputs?: Record; @@ -206,6 +209,7 @@ export const JobSchema = Joi.object({ applicationArchivePath: Joi.string(), username: Joi.string(), + hooks: HooksSchema, experimental: Joi.object({ prebuildCommand: Joi.string(), From 2d7e1993c1c9d22d491a3bc6a20a9da33a014a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Tue, 17 Feb 2026 22:19:01 +0100 Subject: [PATCH 2/2] Fixup --- packages/eas-build-job/src/common.ts | 15 ++++++--------- yarn.lock | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/eas-build-job/src/common.ts b/packages/eas-build-job/src/common.ts index 7604aa23f4..3a20a9407a 100644 --- a/packages/eas-build-job/src/common.ts +++ b/packages/eas-build-job/src/common.ts @@ -148,15 +148,12 @@ export const HooksSchema = Joi.object().pattern( Joi.array() .items(Joi.any()) .required() - .custom( - steps => { - if (steps.length === 0) { - return steps; - } - return validateSteps(steps); - }, - 'steps validation' - ) + .custom(steps => { + if (steps.length === 0) { + return steps; + } + return validateSteps(steps); + }, 'steps validation') ); export const HooksZ = z.record(z.string(), z.array(StepZ)); diff --git a/yarn.lock b/yarn.lock index a858dcb5f2..44416a2ee6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6004,7 +6004,7 @@ bplist-parser@0.3.1: dependencies: big-integer "1.6.x" -bplist-parser@^0.3.0: +bplist-parser@0.3.2, bplist-parser@^0.3.0: version "0.3.2" resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.3.2.tgz#3ac79d67ec52c4c107893e0237eb787cbacbced7" integrity sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==