scaffolder: enable each property on task step (#8890)
Signed-off-by: Alex Eftimie <alex.eftimie@getyourguide.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user