From a0303ee5ef95977dca3c8dbbe98b287af82c1ddb Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Wed, 7 Jun 2023 03:46:41 +0200 Subject: [PATCH] scaffolder: enable each property on task step (#8890) Signed-off-by: Alex Eftimie --- .../tasks/NunjucksWorkflowRunner.test.ts | 33 +++++++++ .../tasks/NunjucksWorkflowRunner.ts | 70 ++++++++++++++----- plugins/scaffolder-common/src/TaskSpec.ts | 6 +- plugins/scaffolder-node/src/actions/types.ts | 5 ++ 4 files changed, 94 insertions(+), 20 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 5d4f7b73cc..8fbbd45b94 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -573,6 +573,39 @@ describe('DefaultWorkflowRunner', () => { }); }); + describe('each', () => { + it('should run a step repeatedly', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + each: '${{parameters.colors}}', + action: 'jest-mock-action', + input: { color: '${{each}}' }, + }, + ], + output: {}, + parameters: { + colors: ['blue', 'green', 'red'], + }, + }); + + await runner.execute(task); + + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ input: { color: 'blue' } }), + ); + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ input: { color: 'green' } }), + ); + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ input: { color: 'red' } }), + ); + }); + }); + describe('secrets', () => { it('should pass through the secrets to the context', async () => { const task = createMockTaskWithSpec( diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index da690bcb82..e3233483ef 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -76,6 +76,7 @@ type TemplateContext = { entity?: UserEntity; ref?: string; }; + each?: JsonValue; }; const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta3 => { @@ -311,25 +312,56 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { const tmpDirs = new Array(); const stepOutput: { [outputName: string]: JsonValue } = {}; - await action.handler({ - input, - secrets: task.secrets ?? {}, - logger: taskLogger, - logStream: streamLogger, - workspacePath, - createTemporaryDirectory: async () => { - const tmpDir = await fs.mkdtemp(`${workspacePath}_step-${step.id}-`); - tmpDirs.push(tmpDir); - return tmpDir; - }, - output(name: string, value: JsonValue) { - stepOutput[name] = value; - }, - templateInfo: task.spec.templateInfo, - user: task.spec.user, - isDryRun: task.isDryRun, - signal: task.cancelSignal, - }); + const iterations = new Array(); + if (step.each) { + const each = await this.render(step.each, context, renderTemplate); + iterations.push(...each); + } else { + iterations.push({}); + } + + let actionInput = input; + for (const iteration of iterations) { + if (step.each) { + taskLogger.info(`Running step each: ${iteration}`); + context.each = iteration; + // re-render input with the modified context that includes each + actionInput = + (step.input && + this.render( + step.input, + { ...context, secrets: task.secrets ?? {} }, + renderTemplate, + )) ?? + {}; + } + await action.handler({ + input: actionInput, + secrets: task.secrets ?? {}, + logger: taskLogger, + logStream: streamLogger, + workspacePath, + createTemporaryDirectory: async () => { + const tmpDir = await fs.mkdtemp( + `${workspacePath}_step-${step.id}-`, + ); + tmpDirs.push(tmpDir); + return tmpDir; + }, + output(name: string, value: JsonValue) { + if (step.each) { + stepOutput[name] = stepOutput[name] || []; + stepOutput[name].push(value); + } else { + stepOutput[name] = value; + } + }, + templateInfo: task.spec.templateInfo, + user: task.spec.user, + isDryRun: task.isDryRun, + signal: task.cancelSignal, + }); + } // Remove all temporary directories that were created when executing the action for (const tmpDir of tmpDirs) { diff --git a/plugins/scaffolder-common/src/TaskSpec.ts b/plugins/scaffolder-common/src/TaskSpec.ts index 7c06fe2b99..6b144b4f78 100644 --- a/plugins/scaffolder-common/src/TaskSpec.ts +++ b/plugins/scaffolder-common/src/TaskSpec.ts @@ -15,7 +15,7 @@ */ import type { EntityMeta, UserEntity } from '@backstage/catalog-model'; -import type { JsonObject, JsonValue } from '@backstage/types'; +import type { JsonArray, JsonObject, JsonValue } from '@backstage/types'; /** * Information about a template that is stored on a task specification. @@ -70,6 +70,10 @@ export interface TaskStep { * When this is false, or if the templated value string evaluates to something that is falsy the step will be skipped. */ if?: string | boolean; + /** + * Run step repeatedly + */ + each?: string | JsonArray; } /** diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index e35f55aa67..7e8c2b4f5f 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -71,6 +71,11 @@ export type ActionContext< * Implement the signal to make your custom step abortable https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal */ signal?: AbortSignal; + + /** + * Optional value of each invocation + */ + each?: JsonObject; }; /** @public */