Merge branch 'master' of https://github.com/backstage/backstage into migrate-scaffolder-to-metrics-service
This commit is contained in:
@@ -1,5 +1,42 @@
|
||||
# @backstage/plugin-scaffolder-backend
|
||||
|
||||
## 3.3.0-next.1
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 309b712: Added a new `execute-template` actions registry action that executes a scaffolder template with provided input values and returns a task ID for tracking progress.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4559806: Removed unnecessary empty `examples` array from actions bridged via the actions registry.
|
||||
- Updated dependencies
|
||||
- @backstage/backend-plugin-api@1.9.0-next.1
|
||||
- @backstage/backend-openapi-utils@0.6.8-next.1
|
||||
- @backstage/plugin-catalog-node@2.1.1-next.1
|
||||
- @backstage/plugin-events-node@0.4.21-next.1
|
||||
- @backstage/plugin-permission-node@0.10.12-next.1
|
||||
- @backstage/plugin-scaffolder-node@0.13.1-next.1
|
||||
|
||||
## 3.2.1-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 79453c0: Updated dependency `wait-for-expect` to `^4.0.0`.
|
||||
- Updated dependencies
|
||||
- @backstage/backend-plugin-api@1.8.1-next.0
|
||||
- @backstage/plugin-permission-node@0.10.12-next.0
|
||||
- @backstage/backend-openapi-utils@0.6.8-next.0
|
||||
- @backstage/plugin-catalog-node@2.1.1-next.0
|
||||
- @backstage/plugin-events-node@0.4.21-next.0
|
||||
- @backstage/plugin-scaffolder-node@0.13.1-next.0
|
||||
- @backstage/catalog-model@1.7.7
|
||||
- @backstage/config@1.3.6
|
||||
- @backstage/errors@1.2.7
|
||||
- @backstage/integration@2.0.0
|
||||
- @backstage/types@1.2.2
|
||||
- @backstage/plugin-permission-common@0.9.7
|
||||
- @backstage/plugin-scaffolder-common@2.0.0
|
||||
|
||||
## 3.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-scaffolder-backend",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.0-next.1",
|
||||
"description": "The Backstage backend plugin that helps you create new things",
|
||||
"backstage": {
|
||||
"role": "backend-plugin",
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createExecuteTemplateAction } from './createExecuteTemplateAction';
|
||||
import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
import { scaffolderServiceMock } from '@backstage/plugin-scaffolder-node/testUtils';
|
||||
|
||||
describe('createExecuteTemplateAction', () => {
|
||||
const mockScaffolderService = scaffolderServiceMock.mock();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should scaffold a template and return the taskId', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
mockScaffolderService.scaffold.mockResolvedValue({
|
||||
taskId: 'task-abc-123',
|
||||
});
|
||||
|
||||
createExecuteTemplateAction({
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
scaffolderService: mockScaffolderService,
|
||||
});
|
||||
|
||||
const result = await mockActionsRegistry.invoke({
|
||||
id: 'test:execute-template',
|
||||
input: {
|
||||
templateRef: 'template:default/my-template',
|
||||
values: { name: 'my-app', owner: 'team-a' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.output).toEqual({ taskId: 'task-abc-123' });
|
||||
|
||||
expect(mockScaffolderService.scaffold).toHaveBeenCalledWith(
|
||||
{
|
||||
templateRef: 'template:default/my-template',
|
||||
values: { name: 'my-app', owner: 'team-a' },
|
||||
},
|
||||
{ credentials: expect.anything() },
|
||||
);
|
||||
});
|
||||
|
||||
it('should forward empty values and pass secrets to the scaffolder service', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
mockScaffolderService.scaffold.mockResolvedValue({
|
||||
taskId: 'task-with-secrets',
|
||||
});
|
||||
|
||||
createExecuteTemplateAction({
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
scaffolderService: mockScaffolderService,
|
||||
});
|
||||
|
||||
const result = await mockActionsRegistry.invoke({
|
||||
id: 'test:execute-template',
|
||||
input: {
|
||||
templateRef: 'template:default/secret-template',
|
||||
values: {},
|
||||
secrets: { apiKey: 'super-secret' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.output).toEqual({ taskId: 'task-with-secrets' });
|
||||
expect(mockScaffolderService.scaffold).toHaveBeenCalledWith(
|
||||
{
|
||||
templateRef: 'template:default/secret-template',
|
||||
values: {},
|
||||
secrets: { apiKey: 'super-secret' },
|
||||
},
|
||||
{ credentials: expect.anything() },
|
||||
);
|
||||
});
|
||||
|
||||
it('should not include secrets when not provided', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
mockScaffolderService.scaffold.mockResolvedValue({
|
||||
taskId: 'task-no-secrets',
|
||||
});
|
||||
|
||||
createExecuteTemplateAction({
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
scaffolderService: mockScaffolderService,
|
||||
});
|
||||
|
||||
await mockActionsRegistry.invoke({
|
||||
id: 'test:execute-template',
|
||||
input: {
|
||||
templateRef: 'template:default/my-template',
|
||||
values: { name: 'my-app' },
|
||||
},
|
||||
});
|
||||
|
||||
const scaffoldCall = mockScaffolderService.scaffold.mock.calls[0][0];
|
||||
expect(scaffoldCall).not.toHaveProperty('secrets');
|
||||
});
|
||||
|
||||
it('should propagate errors from the scaffolder service', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
mockScaffolderService.scaffold.mockRejectedValue(
|
||||
new Error('Permission denied'),
|
||||
);
|
||||
|
||||
createExecuteTemplateAction({
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
scaffolderService: mockScaffolderService,
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockActionsRegistry.invoke({
|
||||
id: 'test:execute-template',
|
||||
input: {
|
||||
templateRef: 'template:default/my-template',
|
||||
values: { name: 'my-app' },
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow('Permission denied');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { ScaffolderService } from '@backstage/plugin-scaffolder-node';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
|
||||
export const createExecuteTemplateAction = ({
|
||||
actionsRegistry,
|
||||
scaffolderService,
|
||||
}: {
|
||||
actionsRegistry: ActionsRegistryService;
|
||||
scaffolderService: ScaffolderService;
|
||||
}) => {
|
||||
actionsRegistry.register({
|
||||
name: 'execute-template',
|
||||
title: 'Execute Scaffolder Template',
|
||||
attributes: {
|
||||
destructive: true,
|
||||
readOnly: false,
|
||||
idempotent: false,
|
||||
},
|
||||
description: `Executes a Scaffolder template with its template ref and input parameter values.
|
||||
The template is run using the credentials provided to this action, and respects any RBAC permissions associated with those credentials.
|
||||
Returns a taskId that can be used to track execution progress.
|
||||
Use the catalog.get-catalog-entity action to fetch the Template entity and discover its parameter schema and secrets definition before calling this action.`,
|
||||
schema: {
|
||||
input: z =>
|
||||
z.object({
|
||||
templateRef: z
|
||||
.string()
|
||||
.describe(
|
||||
'The template entity reference to execute, e.g. "template:default/my-template"',
|
||||
),
|
||||
values: z
|
||||
.record(z.unknown())
|
||||
.describe(
|
||||
'Input parameter values required by the template. Use catalog.get-catalog-entity to discover the required parameters for the template.',
|
||||
),
|
||||
secrets: z
|
||||
.record(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
'Optional secrets to pass to the template execution. Use catalog.get-catalog-entity to discover the secrets definition on the Template entity.',
|
||||
),
|
||||
}),
|
||||
output: z =>
|
||||
z.object({
|
||||
taskId: z
|
||||
.string()
|
||||
.describe(
|
||||
'The task ID for the scaffolder execution. Use this to track progress or retrieve logs.',
|
||||
),
|
||||
}),
|
||||
},
|
||||
action: async ({ input, credentials }) => {
|
||||
const { taskId } = await scaffolderService.scaffold(
|
||||
{
|
||||
templateRef: input.templateRef,
|
||||
values: input.values as Record<string, JsonValue>,
|
||||
...(input.secrets && { secrets: input.secrets }),
|
||||
},
|
||||
{ credentials },
|
||||
);
|
||||
|
||||
return { output: { taskId } };
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -19,6 +19,7 @@ import { createListScaffolderTasksAction } from './listScaffolderTasksAction';
|
||||
import { ScaffolderService } from '@backstage/plugin-scaffolder-node';
|
||||
import { createDryRunTemplateAction } from './createDryRunTemplateAction';
|
||||
import { createListScaffolderActionsAction } from './createListScaffolderActionsAction';
|
||||
import { createExecuteTemplateAction } from './createExecuteTemplateAction';
|
||||
import { createGetScaffolderTaskLogsAction } from './createGetScaffolderTaskLogsAction';
|
||||
|
||||
export const createScaffolderActions = (options: {
|
||||
@@ -33,5 +34,6 @@ export const createScaffolderActions = (options: {
|
||||
});
|
||||
createDryRunTemplateAction(options);
|
||||
createListScaffolderActionsAction(options);
|
||||
createExecuteTemplateAction(options);
|
||||
createGetScaffolderTaskLogsAction(options);
|
||||
};
|
||||
|
||||
@@ -93,7 +93,6 @@ export class DefaultTemplateActionRegistry implements TemplateActionRegistry {
|
||||
ret.set(action.id, {
|
||||
id: action.id,
|
||||
description: action.description,
|
||||
examples: [],
|
||||
supportsDryRun:
|
||||
action.attributes?.readOnly === true &&
|
||||
action.attributes?.destructive === false,
|
||||
|
||||
@@ -285,7 +285,6 @@ describe('scaffolder router', () => {
|
||||
|
||||
expect(response.body).toContainEqual({
|
||||
description: 'Test',
|
||||
examples: [],
|
||||
id: 'test:my-demo-action',
|
||||
schema: {
|
||||
input: {
|
||||
|
||||
Reference in New Issue
Block a user