From 77bee9f0134b9107019f38ce9d52af4d59b362b0 Mon Sep 17 00:00:00 2001 From: John Collier Date: Wed, 4 Mar 2026 16:25:29 -0500 Subject: [PATCH] feat(scaffolder): Allow sorting by status in scaffolderService.listTasks. Added optional `status` filter to `ScaffolderService.listTasks`, by exposing the `status` query parameter, allowing callers to retrieve tasks of a specific status. Also updated the `list-scaffolder-tasks` action to support this parameter. Signed-off-by: John Collier --- .changeset/fifty-clubs-play.md | 5 ++ .changeset/gold-friends-end.md | 5 ++ .../actions/listScaffolderTasksAction.test.ts | 55 ++++++++++++++++++- .../src/actions/listScaffolderTasksAction.ts | 12 ++++ .../scaffolder-node/src/scaffolderService.ts | 5 ++ 5 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 .changeset/fifty-clubs-play.md create mode 100644 .changeset/gold-friends-end.md diff --git a/.changeset/fifty-clubs-play.md b/.changeset/fifty-clubs-play.md new file mode 100644 index 0000000000..561e24a130 --- /dev/null +++ b/.changeset/fifty-clubs-play.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Updated the `list-scaffolder-tasks` action to support the new "status" filter paramter, allowing the action to return tasks matching a specific status. diff --git a/.changeset/gold-friends-end.md b/.changeset/gold-friends-end.md new file mode 100644 index 0000000000..eddacadf19 --- /dev/null +++ b/.changeset/gold-friends-end.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-node': minor +--- + +Added optional `status` filter to `ScaffolderService.listTasks`, allowing callers to retrieve tasks matching a specific status. diff --git a/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.test.ts b/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.test.ts index 075ba919f1..bad0b366b6 100644 --- a/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.test.ts +++ b/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.test.ts @@ -63,7 +63,12 @@ describe('createListScaffolderTasksAction', () => { totalTasks: mockTasks.totalTasks ?? 0, }); expect(mockScaffolderService.listTasks).toHaveBeenCalledWith( - { createdBy: undefined, limit: undefined, offset: undefined }, + { + createdBy: undefined, + limit: undefined, + offset: undefined, + status: undefined, + }, expect.objectContaining({ credentials: expect.anything() }), ); }); @@ -106,7 +111,7 @@ describe('createListScaffolderTasksAction', () => { }); expect(mockScaffolderService.listTasks).toHaveBeenCalledWith( - { createdBy: undefined, limit: 2, offset: 1 }, + { createdBy: undefined, limit: 2, offset: 1, status: undefined }, expect.objectContaining({ credentials: expect.anything() }), ); @@ -188,11 +193,57 @@ describe('createListScaffolderTasksAction', () => { createdBy: 'user:default/alice', limit: undefined, offset: undefined, + status: undefined, }, expect.objectContaining({ credentials: expect.anything() }), ); }); + it('should filter tasks by status when status is provided', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockAuth = mockServices.auth.mock(); + const mockScaffolderService = scaffolderServiceMock.mock(); + const completedTasks = generateMockTasks().tasks.filter( + t => t.status === 'completed', + ); + + mockScaffolderService.listTasks.mockResolvedValue({ + items: completedTasks as ScaffolderTask[], + totalItems: completedTasks.length, + }); + + createListScaffolderTasksAction({ + actionsRegistry: mockActionsRegistry, + auth: mockAuth, + scaffolderService: mockScaffolderService, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:list-scaffolder-tasks', + input: { status: 'completed' }, + }); + + expect(mockScaffolderService.listTasks).toHaveBeenCalledWith( + { + createdBy: undefined, + limit: undefined, + offset: undefined, + status: 'completed', + }, + expect.objectContaining({ credentials: expect.anything() }), + ); + expect(result.output).toEqual({ + tasks: completedTasks.map(task => ({ + id: task.id, + spec: task.spec, + status: task.status, + createdAt: task.createdAt, + lastHeartbeatAt: task.lastHeartbeatAt, + })), + totalTasks: completedTasks.length, + }); + }); + it('should throw NotAllowedError when owned is true without user identity', async () => { const mockActionsRegistry = actionsRegistryServiceMock(); const mockAuth = mockServices.auth.mock(); diff --git a/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.ts b/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.ts index 70977c9d12..2b05b8aee1 100644 --- a/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.ts +++ b/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.ts @@ -65,6 +65,17 @@ Pagination is supported via limit and offset. .min(0) .describe('The offset to start from for pagination') .optional(), + status: z + .enum([ + 'open', + 'processing', + 'completed', + 'failed', + 'cancelled', + 'skipped', + ]) + .optional() + .describe('Filter tasks by status'), }), output: z => z @@ -112,6 +123,7 @@ Pagination is supported via limit and offset. createdBy, limit: input.limit, offset: input.offset, + status: input.status, }, { credentials }, ); diff --git a/plugins/scaffolder-node/src/scaffolderService.ts b/plugins/scaffolder-node/src/scaffolderService.ts index b978164c7f..74d301b718 100644 --- a/plugins/scaffolder-node/src/scaffolderService.ts +++ b/plugins/scaffolder-node/src/scaffolderService.ts @@ -83,6 +83,7 @@ export interface ScaffolderService { createdBy?: string; limit?: number; offset?: number; + status?: ScaffolderTaskStatus; }, options: ScaffolderServiceRequestOptions, ): Promise<{ items: ScaffolderTask[]; totalItems: number }>; @@ -185,6 +186,7 @@ class DefaultScaffolderService implements ScaffolderService { createdBy?: string; limit?: number; offset?: number; + status?: ScaffolderTaskStatus; }, options: ScaffolderServiceRequestOptions, ): Promise<{ items: ScaffolderTask[]; totalItems: number }> { @@ -201,6 +203,9 @@ class DefaultScaffolderService implements ScaffolderService { if (request.offset !== undefined) { params.set('offset', String(request.offset)); } + if (request.status !== undefined) { + params.set('status', request.status); + } const query = params.toString(); const url = `${baseUrl}/v2/tasks${query ? `?${query}` : ''}`;