Skip to content
Closed
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 @@ -9,6 +9,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features

- [eas-cli] Validate local composite functions referenced from workflow job hooks during `eas workflow:validate`. ([#4064](https://github.com/expo/eas-cli/pull/4064) by [@sswrk](https://github.com/sswrk))
- [build-tools] Load local functions declaring `command` or `path`, so custom build functions moved from `.eas/build` configs into `.eas/functions` can be called from workflows. ([#4097](https://github.com/expo/eas-cli/pull/4097) by [@sswrk](https://github.com/sswrk))

### 🐛 Bug fixes

Expand Down
8 changes: 4 additions & 4 deletions packages/build-tools/src/builders/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
BuildStepGlobalContext,
BuildWorkflow,
StepsConfigParser,
buildLocalCompositeFunctionCatalogAsync,
createLocalCompositeFunctionLoader,
buildLocalFunctionCatalogAsync,
createLocalFunctionLoader,
errors,
} from '@expo/steps';
import assert from 'assert';
Expand Down Expand Up @@ -70,11 +70,11 @@ export async function runCustomBuildAsync(ctx: BuildContext<BuildJob>): Promise<
steps: ctx.job.steps,
hooks: ctx.job.hooks,
// Eager for job steps (always run), lazy loader for hooks (running anchors only).
compositeFunctionCatalog: await buildLocalCompositeFunctionCatalogAsync(projectRoot, {
localFunctionCatalog: await buildLocalFunctionCatalogAsync(projectRoot, {
rootSteps: ctx.job.steps,
logger: ctx.logger,
}),
loadCompositeFunction: createLocalCompositeFunctionLoader(projectRoot, {
loadLocalFunction: createLocalFunctionLoader(projectRoot, {
logger: ctx.logger,
}),
})
Expand Down
21 changes: 10 additions & 11 deletions packages/build-tools/src/common/jobHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
BuildStepGlobalContext,
HookEntry,
constructHookEntriesAsync,
createLocalCompositeFunctionLoader,
extendCompositeFunctionCatalogFromStepsAsync,
createLocalFunctionLoader,
extendLocalFunctionCatalogFromStepsAsync,
validateHookStepsAsync,
} from '@expo/steps';

Expand Down Expand Up @@ -98,11 +98,10 @@ export async function parseJobHooksAsync<TJob extends BuildJob>(
// outputs accumulate across keys.
const hookEntriesByKey: Partial<Record<HookKey, HookEntry[]>> = {};
const orderedSteps: BuildStep[] = [];
const compositeFunctionCatalog: LocalFunctionCatalog = {};
const loadCompositeFunction = createLocalCompositeFunctionLoader(
ctx.getReactNativeProjectDirectory(),
{ logger: ctx.logger }
);
const localFunctionCatalog: LocalFunctionCatalog = {};
const loadLocalFunction = createLocalFunctionLoader(ctx.getReactNativeProjectDirectory(), {
logger: ctx.logger,
});
for (const anchor of wrappedAnchors) {
for (const side of ['before', 'after'] as const) {
const key: HookKey = `${side}_${anchor}`;
Expand All @@ -113,15 +112,15 @@ export async function parseJobHooksAsync<TJob extends BuildJob>(
let entries: HookEntry[];
try {
// Extended per key so a bad `uses:` path is attributed to that hook key.
await extendCompositeFunctionCatalogFromStepsAsync({
catalog: compositeFunctionCatalog,
await extendLocalFunctionCatalogFromStepsAsync({
catalog: localFunctionCatalog,
rootSteps: steps,
loadCompositeFunction,
loadLocalFunction,
});
entries = await constructHookEntriesAsync(globalContext, steps, {
externalFunctions,
externalFunctionGroups,
compositeFunctionCatalog,
localFunctionCatalog,
});
} catch (err) {
throw new UserError(
Expand Down
10 changes: 5 additions & 5 deletions packages/build-tools/src/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
BuildStepGlobalContext,
BuildWorkflow,
StepsConfigParser,
buildLocalCompositeFunctionCatalogAsync,
createLocalCompositeFunctionLoader,
buildLocalFunctionCatalogAsync,
createLocalFunctionLoader,
errors,
} from '@expo/steps';
import fs from 'fs/promises';
Expand Down Expand Up @@ -54,7 +54,7 @@ export async function runGenericJobAsync(
try {
const projectRoot = ctx.getReactNativeProjectDirectory(customBuildCtx.projectSourceDirectory);
// Eager for job steps (always run), lazy loader for hooks (running anchors only).
const compositeFunctionCatalog = await buildLocalCompositeFunctionCatalogAsync(projectRoot, {
const localFunctionCatalog = await buildLocalFunctionCatalogAsync(projectRoot, {
rootSteps: ctx.job.steps,
logger: ctx.logger,
});
Expand All @@ -64,8 +64,8 @@ export async function runGenericJobAsync(
externalFunctionGroups: getEasFunctionGroups(customBuildCtx),
steps: ctx.job.steps,
hooks: ctx.job.hooks,
compositeFunctionCatalog,
loadCompositeFunction: createLocalCompositeFunctionLoader(projectRoot, {
localFunctionCatalog,
loadLocalFunction: createLocalFunctionLoader(projectRoot, {
logger: ctx.logger,
}),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { promises as fs } from 'fs';
import os from 'os';
import path from 'path';

import { validateWorkflowLocalCompositeFunctionsAsync } from '../compositeFunctions';
import { validateWorkflowLocalFunctionsAsync } from '../localFunctions';

async function makeProjectWithCompositeFunctionAsync(
projectRoot: string,
Expand All @@ -14,7 +14,7 @@ async function makeProjectWithCompositeFunctionAsync(
await fs.writeFile(path.join(functionDir, 'function.yml'), contents, 'utf-8');
}

describe(validateWorkflowLocalCompositeFunctionsAsync, () => {
describe(validateWorkflowLocalFunctionsAsync, () => {
it('validates referenced local composite functions', async () => {
const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-workflow-functions-test-'));
await makeProjectWithCompositeFunctionAsync(
Expand All @@ -31,7 +31,7 @@ describe(validateWorkflowLocalCompositeFunctionsAsync, () => {
};

await expect(
validateWorkflowLocalCompositeFunctionsAsync(workflow, projectRoot)
validateWorkflowLocalFunctionsAsync(workflow, projectRoot)
).resolves.toBeUndefined();
});

Expand All @@ -45,9 +45,7 @@ describe(validateWorkflowLocalCompositeFunctionsAsync, () => {
},
};

await expect(
validateWorkflowLocalCompositeFunctionsAsync(workflow, projectRoot)
).rejects.toThrow(
await expect(validateWorkflowLocalFunctionsAsync(workflow, projectRoot)).rejects.toThrow(
/Local composite function "\.\/\.eas\/functions\/setup" was referenced by a step but no such composite function exists/
);
});
Expand All @@ -64,9 +62,9 @@ describe(validateWorkflowLocalCompositeFunctionsAsync, () => {
},
};

await expect(
validateWorkflowLocalCompositeFunctionsAsync(workflow, projectRoot)
).rejects.toThrow(/must not contain interpolation/);
await expect(validateWorkflowLocalFunctionsAsync(workflow, projectRoot)).rejects.toThrow(
/must not contain interpolation/
);
});

it('validates local composite functions referenced from job hooks', async () => {
Expand All @@ -88,7 +86,7 @@ describe(validateWorkflowLocalCompositeFunctionsAsync, () => {
};

await expect(
validateWorkflowLocalCompositeFunctionsAsync(workflow, projectRoot)
validateWorkflowLocalFunctionsAsync(workflow, projectRoot)
).resolves.toBeUndefined();
});

Expand All @@ -105,9 +103,7 @@ describe(validateWorkflowLocalCompositeFunctionsAsync, () => {
},
};

await expect(
validateWorkflowLocalCompositeFunctionsAsync(workflow, projectRoot)
).rejects.toThrow(
await expect(validateWorkflowLocalFunctionsAsync(workflow, projectRoot)).rejects.toThrow(
/Local composite function "\.\/\.eas\/functions\/setup" was referenced by a step but no such composite function exists/
);
});
Expand All @@ -131,7 +127,7 @@ describe(validateWorkflowLocalCompositeFunctionsAsync, () => {
};

await expect(
validateWorkflowLocalCompositeFunctionsAsync(workflow, projectRoot)
validateWorkflowLocalFunctionsAsync(workflow, projectRoot)
).resolves.toBeUndefined();
});

Expand All @@ -158,7 +154,7 @@ describe(validateWorkflowLocalCompositeFunctionsAsync, () => {
};

await expect(
validateWorkflowLocalCompositeFunctionsAsync(workflow, projectRoot)
validateWorkflowLocalFunctionsAsync(workflow, projectRoot)
).resolves.toBeUndefined();
});

Expand All @@ -179,9 +175,7 @@ describe(validateWorkflowLocalCompositeFunctionsAsync, () => {
},
};

await expect(
validateWorkflowLocalCompositeFunctionsAsync(workflow, projectRoot)
).rejects.toThrow(
await expect(validateWorkflowLocalFunctionsAsync(workflow, projectRoot)).rejects.toThrow(
/Local composite function "\.\/\.eas\/functions\/setup" was referenced by a step but no such composite function exists/
);
});
Expand All @@ -197,13 +191,10 @@ describe(validateWorkflowLocalCompositeFunctionsAsync, () => {
};

await expect(
validateWorkflowLocalCompositeFunctionsAsync({ defaults: 'garbage', jobs }, projectRoot)
validateWorkflowLocalFunctionsAsync({ defaults: 'garbage', jobs }, projectRoot)
).resolves.toBeUndefined();
await expect(
validateWorkflowLocalCompositeFunctionsAsync(
{ defaults: { hooks: 'garbage' }, jobs },
projectRoot
)
validateWorkflowLocalFunctionsAsync({ defaults: { hooks: 'garbage' }, jobs }, projectRoot)
).resolves.toBeUndefined();
});

Expand All @@ -229,7 +220,7 @@ describe(validateWorkflowLocalCompositeFunctionsAsync, () => {
};

await expect(
validateWorkflowLocalCompositeFunctionsAsync(workflow, projectDir)
validateWorkflowLocalFunctionsAsync(workflow, projectDir)
).resolves.toBeUndefined();
});

Expand All @@ -252,9 +243,7 @@ describe(validateWorkflowLocalCompositeFunctionsAsync, () => {
},
};

await expect(
validateWorkflowLocalCompositeFunctionsAsync(workflow, projectDir)
).rejects.toThrow(
await expect(validateWorkflowLocalFunctionsAsync(workflow, projectDir)).rejects.toThrow(
/Local composite function "\.\/\.eas\/functions\/notify" was referenced by a step but no such composite function exists/
);
});
Expand All @@ -279,7 +268,7 @@ describe(validateWorkflowLocalCompositeFunctionsAsync, () => {
};

await expect(
validateWorkflowLocalCompositeFunctionsAsync(workflow, projectDir)
validateWorkflowLocalFunctionsAsync(workflow, projectDir)
).resolves.toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { buildLocalCompositeFunctionCatalogAsync } from '@expo/steps';
import { buildLocalFunctionCatalogAsync } from '@expo/steps';

import Log from '../../log';

export async function validateWorkflowLocalCompositeFunctionsAsync(
export async function validateWorkflowLocalFunctionsAsync(
parsedYaml: any,
projectDir: string
): Promise<void> {
await buildLocalCompositeFunctionCatalogAsync(projectDir, {
await buildLocalFunctionCatalogAsync(projectDir, {
rootSteps: stepsFromWorkflow(parsedYaml),
logger: {
debug: message => {
Expand Down
6 changes: 3 additions & 3 deletions packages/eas-cli/src/commandUtils/workflow/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { promises as fs } from 'fs';
import path from 'path';
import * as YAML from 'yaml';

import { validateWorkflowLocalCompositeFunctionsAsync } from './compositeFunctions';
import { validateWorkflowLocalFunctionsAsync } from './localFunctions';
import { buildProfileNamesFromProjectAsync } from './buildProfileUtils';
import { getExpoApiWorkflowSchemaURL } from '../../api';
import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisionMutation';
Expand Down Expand Up @@ -47,8 +47,8 @@ export async function validateWorkflowFileAsync(
Log.debug(`Validating workflow structure...`);
validateWorkflowStructure(parsedYaml, workflowSchema);

Log.debug(`Validating workflow local composite functions...`);
await validateWorkflowLocalCompositeFunctionsAsync(parsedYaml, projectDir);
Log.debug(`Validating workflow local functions...`);
await validateWorkflowLocalFunctionsAsync(parsedYaml, projectDir);

// Check for other errors using the server-side validation
Log.debug(`Validating workflow on server...`);
Expand Down
6 changes: 2 additions & 4 deletions packages/steps/src/BuildConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +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 { isLocalFunctionPath } from './utils/localCompositeFunctions';
import { BUILD_STEP_OR_BUILD_GLOBAL_CONTEXT_REFERENCE_REGEX } from './utils/template';

export type BuildFunctions = Record<string, BuildFunctionConfig>;
Expand Down Expand Up @@ -439,9 +439,7 @@ export function validateAllFunctionsExist(
}
}
const calledFunctionsOrFunctionGroup = Array.from(calledFunctionsOrFunctionGroupsSet);
const compositeFunctionPaths = calledFunctionsOrFunctionGroup.filter(
isLocalCompositeFunctionPath
);
const compositeFunctionPaths = calledFunctionsOrFunctionGroup.filter(isLocalFunctionPath);
if (compositeFunctionPaths.length > 0) {
throw new BuildConfigError(
`Local composite functions (${compositeFunctionPaths
Expand Down
18 changes: 17 additions & 1 deletion packages/steps/src/BuildStepInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { JobInterpolationContext } from '@expo/eas-build-job';
import assert from 'assert';

import { BuildStepGlobalContext } from './BuildStepContext';
import { BuildStepRuntimeError } from './errors';
import { BuildConfigError, BuildStepRuntimeError } from './errors';
import { interpolateJobContext } from './interpolation';
import {
BUILD_STEP_OR_BUILD_GLOBAL_CONTEXT_REFERENCE_REGEX,
Expand All @@ -26,6 +26,22 @@ export type BuildStepInputValueType<
? number
: Record<string, unknown>;

/** Maps a local function input's declared `type` string onto the enum, shared by both function shapes. */
export function parseBuildStepInputValueTypeName(
type: string,
{ functionPath, inputName }: { functionPath: string; inputName: string }
): BuildStepInputValueTypeName {
const supported = Object.values(BuildStepInputValueTypeName) as string[];
if (!supported.includes(type)) {
throw new BuildConfigError(
`Local function "${functionPath}" input "${inputName}" has unsupported type "${type}". Supported types: ${supported.join(
', '
)}.`
);
}
return type as BuildStepInputValueTypeName;
}

export type BuildStepInputById = Record<string, BuildStepInput>;
export type BuildStepInputProvider = (
ctx: BuildStepGlobalContext,
Expand Down
Loading
Loading