Scaffolder: Provide 'each' context when evaluating 'if' (#32546)
* Scaffolder: Evaluate 'if' conditions inside the context of 'each', if it exists Signed-off-by: Jacob Raihle kdm951 <jacob.raihle@teliacompany.com> * chore: small little cleanup Signed-off-by: benjdlambert <ben@blam.sh> * Reuse pre-iteration context instead of creating it twice Signed-off-by: Jacob Raihle kdm951 <jacob.raihle@teliacompany.com> --------- Signed-off-by: Jacob Raihle kdm951 <jacob.raihle@teliacompany.com> Co-authored-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Support `if` conditions inside `each` loops for scaffolder steps
|
||||
@@ -1066,6 +1066,50 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should run a step repeatedly - only iterations where the "if" condition is truthy', async () => {
|
||||
const truthyConditions = [true, 1, 'a', {}];
|
||||
const falsyConditions = [false, 0, null, ''];
|
||||
const conditions = [...truthyConditions, ...falsyConditions];
|
||||
const task = createMockTaskWithSpec({
|
||||
steps: [
|
||||
{
|
||||
id: 'test',
|
||||
name: 'name',
|
||||
each: '${{parameters.conditions}}',
|
||||
action: 'jest-mock-action',
|
||||
input: { condition: '${{each.value}}' },
|
||||
if: '${{each.value}}',
|
||||
},
|
||||
],
|
||||
parameters: {
|
||||
conditions,
|
||||
},
|
||||
});
|
||||
await runner.execute(task);
|
||||
|
||||
truthyConditions.forEach((condition, idx) => {
|
||||
expectTaskLog(
|
||||
`info: Running step each: {"key":"${idx}","value":"${condition}"}`,
|
||||
);
|
||||
expect(fakeActionHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ input: { condition } }),
|
||||
);
|
||||
});
|
||||
|
||||
falsyConditions.forEach((condition, idx) => {
|
||||
expectTaskLog(
|
||||
`info: Skipping step each: {"key":"${
|
||||
idx + truthyConditions.length
|
||||
}","value":"${condition}"}`,
|
||||
);
|
||||
expect(fakeActionHandler).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ input: { condition } }),
|
||||
);
|
||||
});
|
||||
|
||||
expect(fakeActionHandler).toHaveBeenCalledTimes(truthyConditions.length);
|
||||
});
|
||||
|
||||
it('should run a step repeatedly with validation of single-expression value', async () => {
|
||||
const numbers = [5, 7, 9];
|
||||
const task = createMockTaskWithSpec({
|
||||
|
||||
@@ -270,6 +270,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
if (
|
||||
step.if === false ||
|
||||
(typeof step.if === 'string' &&
|
||||
step.each === undefined &&
|
||||
!isTruthy(this.render(step.if, context, renderTemplate)))
|
||||
) {
|
||||
await stepTrack.skipFalsy();
|
||||
@@ -340,20 +341,18 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
}
|
||||
}
|
||||
|
||||
const preIterationContext = {
|
||||
...context,
|
||||
environment: {
|
||||
parameters: this.environment?.parameters ?? {},
|
||||
secrets: this.environment?.secrets ?? {},
|
||||
},
|
||||
secrets: task.secrets ?? {},
|
||||
};
|
||||
|
||||
const resolvedEach =
|
||||
step.each &&
|
||||
this.render(
|
||||
step.each,
|
||||
{
|
||||
...context,
|
||||
environment: {
|
||||
parameters: this.environment?.parameters || {},
|
||||
secrets: this.environment?.secrets ?? {},
|
||||
},
|
||||
secrets: task?.secrets ?? {},
|
||||
},
|
||||
renderTemplate,
|
||||
);
|
||||
this.render(step.each, preIterationContext, renderTemplate);
|
||||
|
||||
if (step.each && !resolvedEach) {
|
||||
throw new InputError(
|
||||
@@ -367,26 +366,29 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
each: { key, value },
|
||||
}))
|
||||
: [{}]
|
||||
).map(i => ({
|
||||
...i,
|
||||
// Secrets are only passed when templating the input to actions for security reasons
|
||||
input: step.input
|
||||
? this.render(
|
||||
step.input,
|
||||
{
|
||||
...context,
|
||||
environment: {
|
||||
parameters: this.environment?.parameters ?? {},
|
||||
secrets: this.environment?.secrets ?? {},
|
||||
},
|
||||
secrets: task.secrets ?? {},
|
||||
...i,
|
||||
},
|
||||
renderTemplate,
|
||||
)
|
||||
: {},
|
||||
}));
|
||||
).map(i => {
|
||||
const fullContext = { ...preIterationContext, ...i };
|
||||
// Evaluate if condition once per iteration, only when using 'each'
|
||||
const shouldRun =
|
||||
!('each' in i) ||
|
||||
!step.if ||
|
||||
isTruthy(this.render(step.if, fullContext, renderTemplate));
|
||||
|
||||
return {
|
||||
...i,
|
||||
shouldRun,
|
||||
// Secrets are only passed when templating the input to actions for security reasons
|
||||
input: step.input
|
||||
? this.render(step.input, fullContext, renderTemplate)
|
||||
: {},
|
||||
};
|
||||
});
|
||||
for (const iteration of iterations) {
|
||||
if (!iteration.shouldRun) {
|
||||
// No need to check schema or authorization for iterations that will not run
|
||||
continue;
|
||||
}
|
||||
|
||||
const actionId = `${action.id}${
|
||||
iteration.each ? `[${iteration.each.key}]` : ''
|
||||
}`;
|
||||
@@ -424,10 +426,20 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
|
||||
for (const iteration of iterations) {
|
||||
if (iteration.each) {
|
||||
if (!iteration.shouldRun) {
|
||||
taskLogger.info(
|
||||
`Skipping step each: ${JSON.stringify(
|
||||
iteration.each,
|
||||
(k, v) => (k ? String(v) : v),
|
||||
0,
|
||||
)}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
taskLogger.info(
|
||||
`Running step each: ${JSON.stringify(
|
||||
iteration.each,
|
||||
(k, v) => (k ? v.toString() : v),
|
||||
(k, v) => (k ? String(v) : v),
|
||||
0,
|
||||
)}`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user