Merge pull request #30119 from dzemanov/template-secrets-in-each

fix(scaffolder-backend): use secrets within `each` step
This commit is contained in:
Ben Lambert
2025-06-10 15:27:25 +02:00
committed by GitHub
4 changed files with 147 additions and 1 deletions
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Enable usage of secrets within 'each' step of software templates. For example, you can now structure your `each` step like this:
```
each:
[
{ name: "Service1", token: "${{ secrets.token1 }}" },
{ name: "Service2", token: "${{ secrets.token2 }}" },
]
```
@@ -274,6 +274,58 @@ spec:
password: ${{ secrets.password }}
```
You can also consume secrets within `each` step of the template.
```yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: v1beta3-demo
title: Test Action template
description: scaffolder v1beta3 template demo
spec:
owner: backstage/techdocs-core
type: service
parameters:
- title: Authentication
description: Provide authentication for the resource
required:
- service1
- token1
- service2
- token2
properties:
service1:
type: string
token1:
type: string
ui:field: Secret
service2:
type: string
token2:
type: string
ui:field: Secret
steps:
- id: setupAuthentication
action: auth:create
each:
[
{
name: '${{ parameters.service1 }}',
token: '${{ secrets.token1 }}',
},
{
name: '${{ parameters.service2 }}',
token: '${{ secrets.token2 }}',
},
]
input:
name: ${{ each.value.name }}
token: ${{ each.value.token }}
```
### Custom step layouts
If you find that the default layout of the form used in a particular step does not meet your needs then you can supply your own [custom step layout](./writing-custom-step-layouts.md).
@@ -832,6 +832,42 @@ describe('NunjucksWorkflowRunner', () => {
});
});
it('should run a step repeatedly - flat values with secrets', async () => {
const secrets = {
s1: 'secret-value1',
s2: 'secret-value2',
s3: 'secret-value3',
};
const task = createMockTaskWithSpec(
{
steps: [
{
id: 'test',
name: 'name',
each: [
'${{ secrets.s1 }}',
'${{ secrets.s2 }}',
'${{ secrets.s3 }}',
],
action: 'jest-mock-action',
input: { secret: '${{each.value}}' },
},
],
},
secrets,
);
await runner.execute(task);
Object.values(secrets).forEach((secret, idx) => {
expectTaskLog(
`info: Running step each: {"key":"${idx}","value":"***"}`,
);
expect(fakeActionHandler).toHaveBeenCalledWith(
expect.objectContaining({ input: { secret } }),
);
});
});
it('should run a step repeatedly - object list', async () => {
const task = createMockTaskWithSpec({
steps: [
@@ -862,6 +898,46 @@ describe('NunjucksWorkflowRunner', () => {
);
});
it('should run a step repeatedly - object list with secrets', async () => {
const secrets = {
s1: 'secret-value1',
s2: 'secret-value2',
};
const names = ['Service1', 'Service2'];
const task = createMockTaskWithSpec(
{
steps: [
{
id: 'test',
name: 'name',
each: [
{ name: names[0], token: '${{ secrets.s1 }}' },
{ name: names[1], token: '${{ secrets.s2 }}' },
],
action: 'jest-mock-action',
input: {
name: '${{each.value.name}}',
token: '${{each.value.token}}',
},
},
],
},
secrets,
);
await runner.execute(task);
Object.values(secrets).forEach((secret, idx) => {
expectTaskLog(
`info: Running step each: {"key":"${idx}","value":"[object Object]"}`,
);
expect(fakeActionHandler).toHaveBeenCalledWith(
expect.objectContaining({
input: { name: names[idx], token: secret },
}),
);
});
});
it('should run a step repeatedly - object', async () => {
const settings = {
color: 'blue',
@@ -299,7 +299,12 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
}
const resolvedEach =
step.each && this.render(step.each, context, renderTemplate);
step.each &&
this.render(
step.each,
{ ...context, secrets: task.secrets ?? {} },
renderTemplate,
);
if (step.each && !resolvedEach) {
throw new InputError(