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:
Jacob Raihle
2026-02-17 15:40:31 +02:00
committed by GitHub
parent 672b97278a
commit 0ce78b0535
3 changed files with 93 additions and 32 deletions
+5
View File
@@ -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,
)}`,
);