From b4397b864649f8c91dbeb08fef89903c69df771b Mon Sep 17 00:00:00 2001 From: sswrk Date: Wed, 1 Jul 2026 15:40:57 +0200 Subject: [PATCH] [eas-build-job] Add local composite function schema --- .../src/__tests__/compositeFunction.test.ts | 78 ++++++++++++ .../eas-build-job/src/compositeFunction.ts | 114 ++++++++++++++++++ packages/eas-build-job/src/index.ts | 1 + 3 files changed, 193 insertions(+) create mode 100644 packages/eas-build-job/src/__tests__/compositeFunction.test.ts create mode 100644 packages/eas-build-job/src/compositeFunction.ts diff --git a/packages/eas-build-job/src/__tests__/compositeFunction.test.ts b/packages/eas-build-job/src/__tests__/compositeFunction.test.ts new file mode 100644 index 0000000000..cb8b5e4016 --- /dev/null +++ b/packages/eas-build-job/src/__tests__/compositeFunction.test.ts @@ -0,0 +1,78 @@ +import { ZodError } from 'zod'; + +import { CompositeFunctionConfigZ } from '../compositeFunction'; + +describe('CompositeFunctionConfigZ', () => { + it('accepts a valid local composite function config', () => { + const config = { + name: 'Setup', + inputs: [{ name: 'greeting', type: 'string', default_value: 'hello' }], + outputs: { version: { value: '${{ steps.read.outputs.version }}' } }, + runs: { + steps: [{ id: 'read', run: 'set-output version "1.0.0"' }], + }, + }; + expect(CompositeFunctionConfigZ.parse(config)).toEqual(config); + }); + + it('accepts shorthand input names', () => { + const config = { + inputs: ['greeting'], + runs: { + steps: [{ run: 'echo hello' }], + }, + }; + expect(CompositeFunctionConfigZ.parse(config)).toEqual(config); + }); + + it('accepts a minimal config with only runs.steps', () => { + const config = { + runs: { + steps: [{ run: 'echo hello' }], + }, + }; + expect(CompositeFunctionConfigZ.parse(config)).toEqual(config); + }); + + it('applies default input type "string" when omitted', () => { + const config = { + inputs: [{ name: 'greeting' }], + runs: { + steps: [{ run: 'echo hello' }], + }, + }; + expect(CompositeFunctionConfigZ.parse(config)).toEqual({ + inputs: [{ name: 'greeting', type: 'string' }], + runs: { + steps: [{ run: 'echo hello' }], + }, + }); + }); + + it('rejects unknown top-level keys', () => { + const config = { + unknown_field: true, + runs: { + steps: [{ run: 'echo hello' }], + }, + }; + expect(() => CompositeFunctionConfigZ.parse(config)).toThrow(ZodError); + expect(() => CompositeFunctionConfigZ.parse(config)).toThrow(/unknown_field/); + }); + + it('errors when runs.steps is empty', () => { + const config = { + name: 'Broken', + runs: { steps: [] }, + }; + expect(() => CompositeFunctionConfigZ.parse(config)).toThrow(ZodError); + try { + CompositeFunctionConfigZ.parse(config); + } catch (err) { + expect(err).toBeInstanceOf(ZodError); + expect((err as ZodError).issues[0]?.message).toMatch( + /must declare at least one step under "runs.steps"/ + ); + } + }); +}); diff --git a/packages/eas-build-job/src/compositeFunction.ts b/packages/eas-build-job/src/compositeFunction.ts new file mode 100644 index 0000000000..6e2eda2bbb --- /dev/null +++ b/packages/eas-build-job/src/compositeFunction.ts @@ -0,0 +1,114 @@ +/** + * Schema for local composite functions, reusable step groups referenced via `uses:` in EAS + * workflows (`.eas/workflows/*.yml`) or inline job step definitions. + * + * This module defines the shape of a composite function configuration file (`function.yml`). + * Callers that load composite function files format validation errors from `CompositeFunctionConfigZ`. + * Local composite functions are not supported in `.eas/build/*.yml` custom build config files. + */ +import { z } from 'zod'; + +import { StepZ } from './step'; + +const CompositeFunctionInputValueTypeNameZ = z.enum(['string', 'boolean', 'number', 'json']); + +const CompositeFunctionInputValueZ = z.union([ + z.string(), + z.boolean(), + z.number(), + z.array(z.unknown()), + z.record(z.string(), z.unknown()), +]); + +const CompositeFunctionInputZ = z.union([ + z + .string() + .describe('Shorthand for an input name with default type "string" and no default value.'), + z + .object({ + name: z.string(), + type: CompositeFunctionInputValueTypeNameZ.default('string'), + default_value: CompositeFunctionInputValueZ.optional(), + allowed_values: z.array(CompositeFunctionInputValueZ).optional(), + required: z.boolean().optional(), + }) + .strict(), +]); + +const CompositeFunctionOutputZ = z + .object({ + description: z.string().optional(), + /** + * @example + * value: '${{ steps.read.outputs.version }}' + */ + value: z.string().describe('Expression that resolves to the output value.'), + }) + .strict(); + +export const CompositeFunctionConfigZ = z + .object({ + /** + * @example + * name: Setup + */ + name: z.string().optional().describe('Display name of the composite function.'), + description: z.string().optional(), + /** + * @example + * inputs: + * - greeting + * - name: platform + * type: string + * default_value: ios + */ + inputs: z + .array(CompositeFunctionInputZ) + .optional() + .describe('Inputs accepted by the composite function.'), + /** + * @example + * outputs: + * version: + * value: '${{ steps.read.outputs.version }}' + */ + outputs: z + .record(z.string(), CompositeFunctionOutputZ) + .optional() + .describe('Named outputs exposed by the composite function to its caller.'), + runs: z.object({ + /** + * @example + * runs: + * steps: + * - id: read + * run: set-output version "1.0.0" + */ + steps: z + .array(StepZ) + .min(1, { + message: 'A composite function must declare at least one step under "runs.steps".', + }) + .describe('Steps executed when the composite function is invoked.'), + }), + }) + .strict(); + +/** + * Structure of a local composite function configuration file (`function.yml`). + * + * @example + * name: Setup + * inputs: + * - greeting + * outputs: + * version: + * value: '${{ steps.read.outputs.version }}' + * runs: + * steps: + * - id: read + * run: set-output version "1.0.0" + */ +export type CompositeFunctionConfig = z.infer; + +export type CompositeFunctionCatalog = Record; diff --git a/packages/eas-build-job/src/index.ts b/packages/eas-build-job/src/index.ts index 8439477e0e..04691e2ebb 100644 --- a/packages/eas-build-job/src/index.ts +++ b/packages/eas-build-job/src/index.ts @@ -26,6 +26,7 @@ export * from './context'; export * from './generic'; export * from './hooks'; export * from './step'; +export * from './compositeFunction'; export * from './submission-config'; export * from './projectPackage';