diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts index c249606658..6447782654 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts @@ -55,6 +55,23 @@ describe('DefaultWorkflowRunner', () => { handler: fakeActionHandler, }); + actionRegistry.register({ + id: 'jest-validated-action', + description: 'Mock action for testing', + handler: fakeActionHandler, + schema: { + input: { + type: 'object', + required: ['foo'], + properties: { + foo: { + type: 'number', + }, + }, + }, + }, + }); + actionRegistry.register({ id: 'output-action', description: 'Mock action for testing', @@ -85,7 +102,40 @@ describe('DefaultWorkflowRunner', () => { ); }); - describe('validation', () => {}); + describe('validation', () => { + it('should throw an error if the action has a schema and the input does not match', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'backstage.io/v1beta3', + parameters: {}, + output: {}, + steps: [{ id: 'test', name: 'name', action: 'jest-validated-action' }], + }); + + await expect(runner.execute(task)).rejects.toThrowError( + /Invalid input passed to action jest-validated-action, instance requires property \"foo\"/, + ); + }); + + it('should run the action when the validation passes', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'backstage.io/v1beta3', + parameters: {}, + output: {}, + steps: [ + { + id: 'test', + name: 'name', + action: 'jest-validated-action', + input: { foo: 1 }, + }, + ], + }); + + await runner.execute(task); + + expect(fakeActionHandler).toHaveBeenCalledTimes(1); + }); + }); describe('conditionals', () => { it('should execute steps conditionally', async () => { const task = createMockTaskWithSpec({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts index 889048d04a..c08363857d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts @@ -31,6 +31,7 @@ import { JsonObject, JsonValue } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { PassThrough } from 'stream'; import { isTruthy } from './helper'; +import { validate as validateJsonSchema } from 'jsonschema'; type Options = { workingDirectory: string; @@ -79,6 +80,8 @@ export class DefaultWorkflowRunner implements WorkflowRunner { private readonly nunjucks: nunjucks.Environment; constructor(private readonly options: Options) { + // TODO(blam): Probably need the repo helper here. + // Or we move to returning Objects in the RepoUrlPickerV2 or something? this.nunjucks = nunjucks.configure({ autoescape: false, tags: { @@ -164,7 +167,20 @@ export class DefaultWorkflowRunner implements WorkflowRunner { const action = this.options.actionRegistry.get(step.action); const { taskLogger, streamLogger } = createStepLogger({ task, step }); - const input = step.input && this.render(step.input, context); + const input = (step.input && this.render(step.input, context)) ?? {}; + + if (action.schema?.input) { + const validateResult = validateJsonSchema( + input, + action.schema.input, + ); + if (!validateResult.valid) { + const errors = validateResult.errors.join(', '); + throw new InputError( + `Invalid input passed to action ${action.id}, ${errors}`, + ); + } + } const tmpDirs = new Array(); const stepOutput: { [outputName: string]: JsonValue } = {};