feat(scaffolder): create scaffolder mcp action to execute a template (#33124)
* create mcp action to execute a template Signed-off-by: Stephanie <yangcao@redhat.com> * include changeset Signed-off-by: Stephanie <yangcao@redhat.com> * address test errors and review comments from copilot Signed-off-by: Stephanie <yangcao@redhat.com> * address review comments Signed-off-by: Stephanie <yangcao@redhat.com> * add execute-template to mcp actions list Signed-off-by: Stephanie <yangcao@redhat.com> * fix: address review feedback for execute-template action Signed-off-by: benjdlambert <ben@blam.sh> --------- Signed-off-by: Stephanie <yangcao@redhat.com> Signed-off-by: benjdlambert <ben@blam.sh> Co-authored-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
---
|
||||
|
||||
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.
|
||||
@@ -27,4 +27,5 @@ This is a (non-exhaustive) list of actions that are known to be part of the Acti
|
||||
- `scaffolder.dry-run-template` (Dry Run Scaffolder Template): Dry-runs a scaffolder template to validate it without making changes. Returns success with execution logs, or errors for validation failures.
|
||||
- `scaffolder.list-scaffolder-actions` (List Scaffolder Actions): Lists all installed Scaffolder actions.
|
||||
- `scaffolder.list-scaffolder-tasks` (List Scaffolder Tasks): This allows you to list scaffolder tasks that have been created.
|
||||
- `scaffolder.execute-template` (Execute Scaffolder Template): Executes a Scaffolder template with its template ref and input parameter values.
|
||||
- `scaffolder.get-scaffolder-task-logs` (Get Scaffolder Task Logs): This allows you to fetch the logs of a given scaffolder task.
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user