From 1b5e83401ff586caeb65c38b2ea1965af62f5c31 Mon Sep 17 00:00:00 2001 From: pillaris Date: Mon, 27 Apr 2026 15:43:33 +0530 Subject: [PATCH] fix: wrap child group transformer in try/catch to keep burst resilient A throwing custom groupTransformer on a child group member would fail the entire groups-phase burst. Apply the same try/catch + debug/warn logging pattern already used for user member transformation. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: pillaris --- ...softGraphIncrementalEntityProvider.test.ts | 64 +++++++++++++++++++ ...MicrosoftGraphIncrementalEntityProvider.ts | 29 +++++++-- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-backend-module-msgraph-incremental/src/MicrosoftGraphIncrementalEntityProvider.test.ts b/plugins/catalog-backend-module-msgraph-incremental/src/MicrosoftGraphIncrementalEntityProvider.test.ts index 2fbc4bccf5..630b246f81 100644 --- a/plugins/catalog-backend-module-msgraph-incremental/src/MicrosoftGraphIncrementalEntityProvider.test.ts +++ b/plugins/catalog-backend-module-msgraph-incremental/src/MicrosoftGraphIncrementalEntityProvider.test.ts @@ -728,6 +728,70 @@ describe('MicrosoftGraphIncrementalEntityProvider', () => { expect(parentEntity!.entity.spec?.children).toHaveLength(0); }); + it('logs a warning and skips a child group member when the transformer throws', async () => { + const throwingGroupTransformer = jest + .fn() + .mockResolvedValueOnce({ + // first call: parent group transform succeeds + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'parent-group', + annotations: { 'graph.microsoft.com/group-id': 'grp-parent' }, + }, + spec: { type: 'team', children: [], members: [] }, + }) + .mockRejectedValueOnce(new Error('Transformer error')); + + (mockClient.getOrganization as jest.Mock).mockResolvedValue({ + id: 'org-id', + displayName: 'My Org', + }); + mockRequestOnePage.mockResolvedValueOnce({ + items: [ + { + id: 'grp-parent', + displayName: 'Parent', + mail: 'parent@example.com', + }, + ], + nextLink: undefined, + }); + (mockClient.getGroupMembers as jest.Mock).mockReturnValue( + asyncYield({ + '@odata.type': '#microsoft.graph.group', + id: 'grp-child', + displayName: 'Child Group', + mail: 'child@example.com', + }), + ); + + const provider = new MicrosoftGraphIncrementalEntityProvider({ + id: 'default', + provider: baseProviderConfig as any, + logger, + groupTransformer: throwingGroupTransformer, + }); + + const result = await provider.next(makeContext(), groupsCursor); + + // Parent group still emitted despite child transformer throwing + const parentEntity = result.entities!.find( + e => + e.entity.metadata.annotations?.[ + MICROSOFT_GRAPH_GROUP_ID_ANNOTATION + ] === 'grp-parent', + ); + expect(parentEntity).toBeDefined(); + expect(parentEntity!.entity.spec?.children).toHaveLength(0); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining( + 'group member child group grp-child failed to transform, skipping', + ), + expect.anything(), + ); + }); + it('returns done:false with groups cursor when nextLink is present', async () => { const nextLink = 'https://graph.microsoft.com/v1.0/groups?$skiptoken=page2'; diff --git a/plugins/catalog-backend-module-msgraph-incremental/src/MicrosoftGraphIncrementalEntityProvider.ts b/plugins/catalog-backend-module-msgraph-incremental/src/MicrosoftGraphIncrementalEntityProvider.ts index dc333a924f..dedc94b13b 100644 --- a/plugins/catalog-backend-module-msgraph-incremental/src/MicrosoftGraphIncrementalEntityProvider.ts +++ b/plugins/catalog-backend-module-msgraph-incremental/src/MicrosoftGraphIncrementalEntityProvider.ts @@ -481,14 +481,29 @@ export class MicrosoftGraphIncrementalEntityProvider // With a filter, child groups may not be ingested themselves, // which would produce dangling spec.children references. if (!provider.groupFilter && !provider.groupSearch) { - const childEntity = await groupTransformer( - member as MicrosoftGraph.Group, - ); - if (childEntity) { - childEntity.metadata.name = capEntityName( - childEntity.metadata.name, + try { + const childEntity = await groupTransformer( + member as MicrosoftGraph.Group, + ); + if (childEntity) { + childEntity.metadata.name = capEntityName( + childEntity.metadata.name, + ); + childRefs.push(stringifyEntityRef(childEntity)); + } else { + this.options.logger.debug( + `${this.getProviderName()}: group member child group ${ + member.id + } could not be transformed (sparse object?), skipping`, + ); + } + } catch (e) { + this.options.logger.warn( + `${this.getProviderName()}: group member child group ${ + member.id + } failed to transform, skipping`, + { error: e }, ); - childRefs.push(stringifyEntityRef(childEntity)); } } }