From 1badf0f133f5a8568ba4f0a8d800578e15535ba7 Mon Sep 17 00:00:00 2001 From: sswrk Date: Wed, 15 Jul 2026 14:42:06 +0200 Subject: [PATCH] [steps] Add isLocalCompositeFunctionPath and reject local composite functions in custom builds --- packages/steps/src/BuildConfig.ts | 13 +++++++ .../steps/src/__tests__/BuildConfig-test.ts | 34 +++++++++++++++++++ .../__tests__/localCompositeFunctions-test.ts | 15 ++++++++ .../src/utils/localCompositeFunctions.ts | 7 ++++ 4 files changed, 69 insertions(+) create mode 100644 packages/steps/src/utils/__tests__/localCompositeFunctions-test.ts create mode 100644 packages/steps/src/utils/localCompositeFunctions.ts diff --git a/packages/steps/src/BuildConfig.ts b/packages/steps/src/BuildConfig.ts index bba37f0bab..a3af990c94 100644 --- a/packages/steps/src/BuildConfig.ts +++ b/packages/steps/src/BuildConfig.ts @@ -8,6 +8,7 @@ import { BuildRuntimePlatform } from './BuildRuntimePlatform'; import { BuildStepEnv } from './BuildStepEnv'; import { BuildStepInputValueType, BuildStepInputValueTypeName } from './BuildStepInput'; import { BuildConfigError, BuildWorkflowError } from './errors'; +import { isLocalCompositeFunctionPath } from './utils/localCompositeFunctions'; import { BUILD_STEP_OR_BUILD_GLOBAL_CONTEXT_REFERENCE_REGEX } from './utils/template'; export type BuildFunctions = Record; @@ -438,6 +439,18 @@ export function validateAllFunctionsExist( } } const calledFunctionsOrFunctionGroup = Array.from(calledFunctionsOrFunctionGroupsSet); + const compositeFunctionPaths = calledFunctionsOrFunctionGroup.filter( + isLocalCompositeFunctionPath + ); + if (compositeFunctionPaths.length > 0) { + throw new BuildConfigError( + `Local composite functions (${compositeFunctionPaths + .map(compositeFunctionPath => `"${compositeFunctionPath}"`) + .join( + ', ' + )}) are not supported in ".eas/build/*.yml" custom builds. Local composite functions can only be used in EAS workflows (".eas/workflows/*.yml").` + ); + } const externalFunctionIdsSet = new Set(externalFunctionIds); const externalFunctionGroupsIdsSet = new Set(externalFunctionGroupsIds); const nonExistentFunctionsOrFunctionGroups = calledFunctionsOrFunctionGroup.filter( diff --git a/packages/steps/src/__tests__/BuildConfig-test.ts b/packages/steps/src/__tests__/BuildConfig-test.ts index 1d02336d4f..538ceaeef9 100644 --- a/packages/steps/src/__tests__/BuildConfig-test.ts +++ b/packages/steps/src/__tests__/BuildConfig-test.ts @@ -1042,6 +1042,40 @@ describe(validateAllFunctionsExist, () => { validateAllFunctionsExist(buildConfig, { externalFunctionIds: [] }); }).toThrowError(/Calling non-existent functions: "eas\/build"/); }); + test('rejects local composite function references with a dedicated error', () => { + const buildConfig: BuildConfig = { + build: { + steps: ['./.eas/functions/my-function'], + }, + }; + + expect(() => { + validateAllFunctionsExist(buildConfig, { externalFunctionIds: [] }); + }).toThrowError( + /Local composite functions \("\.\/\.eas\/functions\/my-function"\) are not supported in "\.eas\/build\/\*\.yml" custom builds\. Local composite functions can only be used in EAS workflows \("\.eas\/workflows\/\*\.yml"\)\./ + ); + }); + test('rejects object-form local composite function references with a dedicated error', () => { + const buildConfig: BuildConfig = { + build: { + steps: [ + { + './.eas/functions/my-function': { + inputs: { + foo: 'bar', + }, + }, + }, + ], + }, + }; + + expect(() => { + validateAllFunctionsExist(buildConfig, { externalFunctionIds: [] }); + }).toThrow( + /Local composite functions \("\.\/\.eas\/functions\/my-function"\) are not supported in "\.eas\/build\/\*\.yml" custom builds\. Local composite functions can only be used in EAS workflows \("\.eas\/workflows\/\*\.yml"\)\./ + ); + }); test('non-existent namespaced functions with skipNamespacedFunctionsOrFunctionGroupsCheck = false', () => { const buildConfig: BuildConfig = { build: { diff --git a/packages/steps/src/utils/__tests__/localCompositeFunctions-test.ts b/packages/steps/src/utils/__tests__/localCompositeFunctions-test.ts new file mode 100644 index 0000000000..02f00d8a24 --- /dev/null +++ b/packages/steps/src/utils/__tests__/localCompositeFunctions-test.ts @@ -0,0 +1,15 @@ +import { isLocalCompositeFunctionPath } from '../localCompositeFunctions'; + +describe(isLocalCompositeFunctionPath, () => { + it('recognizes relative paths as local composite function paths', () => { + expect(isLocalCompositeFunctionPath('./.eas/functions/setup')).toBe(true); + expect(isLocalCompositeFunctionPath('../../shared/functions/setup')).toBe(true); + expect(isLocalCompositeFunctionPath(' ./.eas/functions/setup/ ')).toBe(true); + }); + + it('rejects function ids and absolute or backslash-prefixed paths', () => { + expect(isLocalCompositeFunctionPath('eas/build')).toBe(false); + expect(isLocalCompositeFunctionPath('/functions/setup')).toBe(false); + expect(isLocalCompositeFunctionPath('..\\functions\\setup')).toBe(false); + }); +}); diff --git a/packages/steps/src/utils/localCompositeFunctions.ts b/packages/steps/src/utils/localCompositeFunctions.ts new file mode 100644 index 0000000000..ab35e99004 --- /dev/null +++ b/packages/steps/src/utils/localCompositeFunctions.ts @@ -0,0 +1,7 @@ +// Local composite functions referenced via `uses: ./path` or `uses: ../path` in EAS workflows. +// Not supported in `.eas/build/*.yml` custom build configs. + +export function isLocalCompositeFunctionPath(uses: string): boolean { + const trimmed = uses.trim(); + return trimmed.startsWith('./') || trimmed.startsWith('../'); +}