scaffolder: enable each property on task step (#8890)

Signed-off-by: Alex Eftimie <alex.eftimie@getyourguide.com>
This commit is contained in:
Alex Eftimie
2023-06-07 03:46:41 +02:00
parent f0c6887bbb
commit a0303ee5ef
4 changed files with 94 additions and 20 deletions
@@ -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(
@@ -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<string>();
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<JsonValue>();
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) {
+5 -1
View File
@@ -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;
}
/**
@@ -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 */