feat(scaffolder): implement a get scaffolder task log action (#33185)
* feat(scaffolder): create get scaffolder task log action Adds a new action to allow retrieving logs from scaffolder tasks, by way of the scaffolderService.getLogs function Signed-off-by: John Collier <jcollier@redhat.com> * Fix typo after rename Signed-off-by: John Collier <jcollier@redhat.com> * Add to list of known actions Signed-off-by: John Collier <jcollier@redhat.com> --------- Signed-off-by: John Collier <jcollier@redhat.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
---
|
||||
|
||||
Adds a new `get-scaffolder-task-logs` action to `@backstage/plugin-scaffolder-backend` that retrieves log events for a given scaffolder task, with optional support for retrieving only new events after a given event ID.
|
||||
@@ -27,3 +27,4 @@ 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.get-scaffolder-task-logs` (Get Scaffolder Task Logs): This allows you to fetch the logs of a given scaffolder task.
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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 { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
import { scaffolderServiceMock } from '@backstage/plugin-scaffolder-node/testUtils';
|
||||
import { LogEvent } from '@backstage/plugin-scaffolder-common';
|
||||
import { createGetScaffolderTaskLogsAction } from './createGetScaffolderTaskLogsAction';
|
||||
|
||||
describe('createGetScaffolderTaskLogsAction', () => {
|
||||
it('should return log events for a task', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
const mockScaffolderService = scaffolderServiceMock.mock();
|
||||
|
||||
const mockEvents: LogEvent[] = [
|
||||
{
|
||||
id: 1,
|
||||
taskId: 'task-1',
|
||||
createdAt: '2025-01-01T00:00:00Z',
|
||||
type: 'log',
|
||||
body: { message: 'Starting step', stepId: 'step-1' },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
taskId: 'task-1',
|
||||
createdAt: '2025-01-01T00:00:01Z',
|
||||
type: 'log',
|
||||
body: { message: 'Step complete', stepId: 'step-1' },
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
taskId: 'task-1',
|
||||
createdAt: '2025-01-01T00:00:02Z',
|
||||
type: 'completion',
|
||||
body: { message: 'Task completed', status: 'completed' },
|
||||
},
|
||||
];
|
||||
|
||||
mockScaffolderService.getLogs.mockResolvedValue(mockEvents);
|
||||
|
||||
createGetScaffolderTaskLogsAction({
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
scaffolderService: mockScaffolderService,
|
||||
});
|
||||
|
||||
const result = await mockActionsRegistry.invoke({
|
||||
id: 'test:get-scaffolder-task-logs',
|
||||
input: { taskId: 'task-1' },
|
||||
});
|
||||
|
||||
expect(result.output).toEqual({
|
||||
events: [
|
||||
{
|
||||
id: 1,
|
||||
taskId: 'task-1',
|
||||
createdAt: '2025-01-01T00:00:00Z',
|
||||
type: 'log',
|
||||
body: {
|
||||
message: 'Starting step',
|
||||
stepId: 'step-1',
|
||||
status: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
taskId: 'task-1',
|
||||
createdAt: '2025-01-01T00:00:01Z',
|
||||
type: 'log',
|
||||
body: {
|
||||
message: 'Step complete',
|
||||
stepId: 'step-1',
|
||||
status: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
taskId: 'task-1',
|
||||
createdAt: '2025-01-01T00:00:02Z',
|
||||
type: 'completion',
|
||||
body: {
|
||||
message: 'Task completed',
|
||||
stepId: undefined,
|
||||
status: 'completed',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mockScaffolderService.getLogs).toHaveBeenCalledWith(
|
||||
{ taskId: 'task-1', after: undefined },
|
||||
expect.objectContaining({ credentials: expect.anything() }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass the after parameter through to the service', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
const mockScaffolderService = scaffolderServiceMock.mock();
|
||||
|
||||
mockScaffolderService.getLogs.mockResolvedValue([]);
|
||||
|
||||
createGetScaffolderTaskLogsAction({
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
scaffolderService: mockScaffolderService,
|
||||
});
|
||||
|
||||
const result = await mockActionsRegistry.invoke({
|
||||
id: 'test:get-scaffolder-task-logs',
|
||||
input: { taskId: 'task-2', after: 42 },
|
||||
});
|
||||
|
||||
expect(result.output).toEqual({ events: [] });
|
||||
expect(mockScaffolderService.getLogs).toHaveBeenCalledWith(
|
||||
{ taskId: 'task-2', after: 42 },
|
||||
expect.objectContaining({ credentials: expect.anything() }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when the service call fails', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
const mockScaffolderService = scaffolderServiceMock.mock();
|
||||
|
||||
mockScaffolderService.getLogs.mockRejectedValue(
|
||||
new Error('Internal Server Error'),
|
||||
);
|
||||
|
||||
createGetScaffolderTaskLogsAction({
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
scaffolderService: mockScaffolderService,
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockActionsRegistry.invoke({
|
||||
id: 'test:get-scaffolder-task-logs',
|
||||
input: { taskId: 'task-3' },
|
||||
}),
|
||||
).rejects.toThrow('Internal Server Error');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
export const createGetScaffolderTaskLogsAction = ({
|
||||
actionsRegistry,
|
||||
scaffolderService,
|
||||
}: {
|
||||
actionsRegistry: ActionsRegistryService;
|
||||
scaffolderService: ScaffolderService;
|
||||
}) => {
|
||||
actionsRegistry.register({
|
||||
name: 'get-scaffolder-task-logs',
|
||||
title: 'Get Scaffolder Task Logs',
|
||||
attributes: {
|
||||
destructive: false,
|
||||
readOnly: true,
|
||||
idempotent: true,
|
||||
},
|
||||
description: `
|
||||
Retrieve the log events for a scaffolder task.
|
||||
Each log event has a type (log, completion, cancelled, or recovered), a body containing a message and optional step ID and status.
|
||||
Use the after parameter to fetch only events after a specific event ID for incremental polling.
|
||||
`,
|
||||
schema: {
|
||||
input: z =>
|
||||
z.object({
|
||||
taskId: z.string().describe('The ID of the scaffolder task'),
|
||||
after: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe(
|
||||
'Return only log events after this event ID for incremental polling',
|
||||
),
|
||||
}),
|
||||
output: z =>
|
||||
z
|
||||
.object({
|
||||
events: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number().describe('The event ID'),
|
||||
taskId: z
|
||||
.string()
|
||||
.describe('The ID of the task this event belongs to'),
|
||||
createdAt: z
|
||||
.string()
|
||||
.describe('Timestamp when the event was created'),
|
||||
type: z
|
||||
.string()
|
||||
.describe(
|
||||
'Event type: log, completion, cancelled, or recovered',
|
||||
),
|
||||
body: z
|
||||
.object({
|
||||
message: z.string().describe('The log message'),
|
||||
stepId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('The step ID associated with this event'),
|
||||
status: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('The task status at the time of this event'),
|
||||
})
|
||||
.describe('The event body'),
|
||||
}),
|
||||
)
|
||||
.describe('The list of log events for the task'),
|
||||
})
|
||||
.describe('Object containing the events array'),
|
||||
},
|
||||
action: async ({ input, credentials }) => {
|
||||
const events = await scaffolderService.getLogs(
|
||||
{ taskId: input.taskId, after: input.after },
|
||||
{ credentials },
|
||||
);
|
||||
|
||||
return {
|
||||
output: {
|
||||
events: events.map(event => ({
|
||||
id: event.id,
|
||||
taskId: event.taskId,
|
||||
createdAt: event.createdAt,
|
||||
type: event.type,
|
||||
body: {
|
||||
message: event.body.message,
|
||||
stepId: event.body.stepId,
|
||||
status: event.body.status,
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -19,6 +19,7 @@ import { createListScaffolderTasksAction } from './listScaffolderTasksAction';
|
||||
import { ScaffolderService } from '@backstage/plugin-scaffolder-node';
|
||||
import { createDryRunTemplateAction } from './createDryRunTemplateAction';
|
||||
import { createListScaffolderActionsAction } from './createListScaffolderActionsAction';
|
||||
import { createGetScaffolderTaskLogsAction } from './createGetScaffolderTaskLogsAction';
|
||||
|
||||
export const createScaffolderActions = (options: {
|
||||
actionsRegistry: ActionsRegistryService;
|
||||
@@ -32,4 +33,5 @@ export const createScaffolderActions = (options: {
|
||||
});
|
||||
createDryRunTemplateAction(options);
|
||||
createListScaffolderActionsAction(options);
|
||||
createGetScaffolderTaskLogsAction(options);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user