From b0ff0d25798c219c4e3e31050250d69fdddaa509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A9rio=20Val=C3=A9rio?= Date: Tue, 7 Oct 2025 14:47:42 +0300 Subject: [PATCH 01/10] Introduce new option in the GH catalog plugin to exclude suspended users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce an optional setting to exclude suspended users from GitHub Enterprise instances. When it’s set to true, suspended users won’t be emitted by the default transform. If a custom transformer is used, it should check if the property `suspendedAt` from the `GithubUser` is defined, in order to exclude such users. This logic was not introduced in the `GithubMultiOrgReaderProcessor.ts`, since the usage there is marked as deprecated. To note that this setting should be used only against GitHub Enterprise instances, the property does not exist in the github.com GraphQL schema, adding it will cause a schema validation error and the syncing of users will fail. Signed-off-by: Valério Valério --- .../src/module.ts | 4 ++++ plugins/catalog-backend-module-github/config.d.ts | 14 ++++++++++++++ .../src/lib/defaultTransformers.ts | 3 +++ .../src/lib/github.ts | 4 ++++ .../processors/GithubMultiOrgReaderProcessor.ts | 1 + .../src/providers/GithubMultiOrgEntityProvider.ts | 13 +++++++++++++ .../src/providers/GithubOrgEntityProvider.ts | 13 +++++++++++++ 7 files changed, 52 insertions(+) diff --git a/plugins/catalog-backend-module-github-org/src/module.ts b/plugins/catalog-backend-module-github-org/src/module.ts index 2d349cb873..e6179e07f1 100644 --- a/plugins/catalog-backend-module-github-org/src/module.ts +++ b/plugins/catalog-backend-module-github-org/src/module.ts @@ -121,6 +121,7 @@ export const catalogModuleGithubOrgEntityProvider = createBackendModule({ alwaysUseDefaultNamespace: definitions.length === 1 && definition.orgs?.length === 1, pageSizes: definition.pageSizes, + excludeSuspendedUsers: definition.excludeSuspendedUsers, }), ); } @@ -133,6 +134,7 @@ function readDefinitionsFromConfig(rootConfig: Config): Array<{ id: string; githubUrl: string; orgs?: string[]; + excludeSuspendedUsers?: boolean; schedule: SchedulerServiceTaskScheduleDefinition; pageSizes?: { teams?: number; @@ -154,6 +156,8 @@ function readDefinitionsFromConfig(rootConfig: Config): Array<{ id: c.getString('id'), githubUrl: c.getString('githubUrl'), orgs: c.getOptionalStringArray('orgs'), + excludeSuspendedUsers: + c.getOptionalBoolean('excludeSuspendedUsers') ?? false, schedule: readSchedulerServiceTaskScheduleDefinitionFromConfig( c.getConfig('schedule'), ), diff --git a/plugins/catalog-backend-module-github/config.d.ts b/plugins/catalog-backend-module-github/config.d.ts index f3cbb15e06..6277f7aa2e 100644 --- a/plugins/catalog-backend-module-github/config.d.ts +++ b/plugins/catalog-backend-module-github/config.d.ts @@ -264,6 +264,13 @@ export interface Config { */ orgs?: string[]; + /** + * (Optional) Only for GitHub Enterprise. Whether to exclude suspended users when querying organization users. + * If true, the defaultTransformer will not return suspended users. + * Default: `false`. + */ + excludeSuspendedUsers?: boolean; + /** * The refresh schedule to use. */ @@ -315,6 +322,13 @@ export interface Config { */ orgs?: string[]; + /** + * (Optional) Only for GitHub Enterprise. Whether to exclude suspended users when querying organization users. + * If true, the defaultTransformer will not return suspended users. + * Default: `false`. + */ + excludeSuspendedUsers?: boolean; + /** * The refresh schedule to use. */ diff --git a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts index 6befd32056..02f5052ae1 100644 --- a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts +++ b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts @@ -62,6 +62,9 @@ export const defaultUserTransformer = async ( item: GithubUser, _ctx: TransformerContext, ): Promise => { + if (item.suspendedAt) { + return undefined; + } const entity: UserEntity = { apiVersion: 'backstage.io/v1alpha1', kind: 'User', diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts index 625fb57bba..b614b4be64 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.ts @@ -117,6 +117,7 @@ export type GithubUser = { email?: string; name?: string; organizationVerifiedDomainEmails?: string[]; + suspendedAt?: string; }; /** @@ -186,9 +187,11 @@ export async function getOrganizationUsers( client: typeof graphql, org: string, tokenType: GithubCredentialType, + excludeSuspendedUsers: boolean = false, userTransformer: UserTransformer = defaultUserTransformer, pageSizes: GithubPageSizes = DEFAULT_PAGE_SIZES, ): Promise<{ users: Entity[] }> { + const suspendedAtField = excludeSuspendedUsers ? 'suspendedAt,' : ''; const query = ` query users($org: String!, $email: Boolean!, $cursor: String, $organizationMembersPageSize: Int!) { organization(login: $org) { @@ -200,6 +203,7 @@ export async function getOrganizationUsers( email @include(if: $email), login, name, + ${suspendedAtField} organizationVerifiedDomainEmails(login: $org) } } diff --git a/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts b/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts index 8af528dfcf..058ce2cb6c 100644 --- a/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts @@ -148,6 +148,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { client, orgConfig.name, tokenType, + false, async (githubUser, ctx): Promise => { const result = this.options.userTransformer ? await this.options.userTransformer(githubUser, ctx) diff --git a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts index 839ca66e40..bb0dd84c6f 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts @@ -174,6 +174,14 @@ export interface GithubMultiOrgEntityProviderOptions { * Reduce these values if hitting RESOURCE_LIMITS_EXCEEDED errors. */ pageSizes?: Partial; + + /** + * Optionally exclude suspended users when querying organization users. + * @defaultValue false + * @remarks + * Only for GitHub Enterprise instances. Will error if used against GitHub.com API. + */ + excludeSuspendedUsers?: boolean; } type CreateDeltaOperation = (entities: Entity[]) => { @@ -221,6 +229,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { events: options.events, alwaysUseDefaultNamespace: options.alwaysUseDefaultNamespace, pageSizes: options.pageSizes, + excludeSuspendedUsers: options.excludeSuspendedUsers, }); provider.schedule(options.schedule); @@ -241,6 +250,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { teamTransformer?: TeamTransformer; alwaysUseDefaultNamespace?: boolean; pageSizes?: Partial; + excludeSuspendedUsers?: boolean; }, ) {} @@ -304,6 +314,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { client, org, tokenType, + this.options.excludeSuspendedUsers, this.options.userTransformer, pageSizes, ); @@ -456,6 +467,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { client, org, tokenType, + this.options.excludeSuspendedUsers, this.options.userTransformer, pageSizes, ); @@ -690,6 +702,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { client, org, tokenType, + this.options.excludeSuspendedUsers, this.options.userTransformer, pageSizes, ); diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts index 6dfa8a07f8..31b6dc338b 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts @@ -130,6 +130,14 @@ export interface GithubOrgEntityProviderOptions { * Optionally include a team transformer for transforming from GitHub teams to Group Entities */ teamTransformer?: TeamTransformer; + + /** + * Optionally exclude suspended users when querying organization users. + * @defaultValue false + * @remarks + * Only for GitHub Enterprise instances. Will error if used against GitHub.com API. + */ + excludeSuspendedUsers?: boolean; } /** @@ -167,6 +175,7 @@ export class GithubOrgEntityProvider implements EntityProvider { userTransformer: options.userTransformer, teamTransformer: options.teamTransformer, events: options.events, + excludeSuspendedUsers: options.excludeSuspendedUsers, }); provider.schedule(options.schedule); @@ -184,6 +193,7 @@ export class GithubOrgEntityProvider implements EntityProvider { githubCredentialsProvider?: GithubCredentialsProvider; userTransformer?: UserTransformer; teamTransformer?: TeamTransformer; + excludeSuspendedUsers?: boolean; }, ) { this.credentialsProvider = @@ -235,6 +245,7 @@ export class GithubOrgEntityProvider implements EntityProvider { client, org, tokenType, + this.options.excludeSuspendedUsers, this.options.userTransformer, ); const { teams } = await getOrganizationTeams( @@ -363,6 +374,7 @@ export class GithubOrgEntityProvider implements EntityProvider { client, org, tokenType, + this.options.excludeSuspendedUsers, this.options.userTransformer, ); @@ -454,6 +466,7 @@ export class GithubOrgEntityProvider implements EntityProvider { client, org, tokenType, + this.options.excludeSuspendedUsers, this.options.userTransformer, ); From 70666ee784c0e775bc5c85a48393b02ed7028fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A9rio=20Val=C3=A9rio?= Date: Tue, 7 Oct 2025 15:30:46 +0300 Subject: [PATCH 02/10] Add test cases for the GitHub getOrganizationUsers call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Valério Valério --- .../src/lib/github.test.ts | 113 +++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts index ca9b3eb692..2af60d2bb1 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts @@ -166,6 +166,54 @@ describe('github', () => { getOrganizationUsers(graphql, 'a', 'token'), ).resolves.toEqual(output); }); + + it('reads members excluding suspended users', async () => { + const input: QueryResponse = { + organization: { + membersWithRole: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + login: 'a', + name: 'b', + bio: 'c', + email: 'd', + avatarUrl: 'e', + suspendedAt: '2025-01-01', + }, + { + login: 'a', + name: 'b', + bio: 'c', + email: 'd', + avatarUrl: 'e', + suspendedAt: undefined, + }, + ], + }, + }, + }; + + const output = { + users: [ + expect.objectContaining({ + metadata: expect.objectContaining({ name: 'a', description: 'c' }), + spec: { + profile: { displayName: 'b', email: 'd', picture: 'e' }, + memberOf: [], + }, + }), + ], + }; + + server.use( + graphqlMsw.query('users', () => HttpResponse.json({ data: input })), + ); + + await expect( + getOrganizationUsers(graphql, 'a', 'token', true), + ).resolves.toEqual(output); + }); }); describe('getOrganizationUsers using custom UserTransformer', () => { @@ -226,7 +274,13 @@ describe('github', () => { ); await expect( - getOrganizationUsers(graphql, 'a', 'token', customUserTransformer), + getOrganizationUsers( + graphql, + 'a', + 'token', + false, + customUserTransformer, + ), ).resolves.toEqual(output); }); @@ -273,12 +327,69 @@ describe('github', () => { graphql, 'a', 'token', + false, customUserTransformer, ); expect(users.users).toHaveLength(1); expect(users).toEqual(output); }); + + it('reads members including suspended users', async () => { + const input: QueryResponse = { + organization: { + membersWithRole: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + login: 'a', + name: 'b', + bio: 'c', + email: 'd', + avatarUrl: 'e', + }, + { + login: 'ab', + name: 'bb', + bio: 'cc', + email: 'dd', + avatarUrl: 'ee', + suspendedAt: '2025-01-01', + }, + ], + }, + }, + }; + + const output = { + users: [ + expect.objectContaining({ + metadata: expect.objectContaining({ + name: 'a-custom', + }), + }), + expect.objectContaining({ + metadata: expect.objectContaining({ + name: 'ab-custom', + }), + }), + ], + }; + + server.use( + graphqlMsw.query('users', () => HttpResponse.json({ data: input })), + ); + + await expect( + getOrganizationUsers( + graphql, + 'a', + 'token', + true, + customUserTransformer, + ), + ).resolves.toEqual(output); + }); }); describe('getOrganizationTeams using default TeamTransformer', () => { From 9d72ca9250fc6ec535d550e7fe2870678b94b8cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A9rio=20Val=C3=A9rio?= Date: Tue, 7 Oct 2025 15:52:23 +0300 Subject: [PATCH 03/10] Generate API report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Valério Valério --- .../report.api.md | 5 +++++ .../src/lib/github.test.ts | 19 +++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/plugins/catalog-backend-module-github/report.api.md b/plugins/catalog-backend-module-github/report.api.md index 016bb6da65..5f140a0d5e 100644 --- a/plugins/catalog-backend-module-github/report.api.md +++ b/plugins/catalog-backend-module-github/report.api.md @@ -151,6 +151,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { teamTransformer?: TeamTransformer; alwaysUseDefaultNamespace?: boolean; pageSizes?: Partial; + excludeSuspendedUsers?: boolean; }); connect(connection: EntityProviderConnection): Promise; // (undocumented) @@ -166,6 +167,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { export interface GithubMultiOrgEntityProviderOptions { alwaysUseDefaultNamespace?: boolean; events?: EventsService; + excludeSuspendedUsers?: boolean; githubCredentialsProvider?: GithubCredentialsProvider; githubUrl: string; id: string; @@ -227,6 +229,7 @@ export class GithubOrgEntityProvider implements EntityProvider { githubCredentialsProvider?: GithubCredentialsProvider; userTransformer?: UserTransformer; teamTransformer?: TeamTransformer; + excludeSuspendedUsers?: boolean; }); connect(connection: EntityProviderConnection): Promise; // (undocumented) @@ -244,6 +247,7 @@ export type GitHubOrgEntityProviderOptions = GithubOrgEntityProviderOptions; // @public export interface GithubOrgEntityProviderOptions { events?: EventsService; + excludeSuspendedUsers?: boolean; githubCredentialsProvider?: GithubCredentialsProvider; id: string; logger: LoggerService; @@ -306,6 +310,7 @@ export type GithubUser = { email?: string; name?: string; organizationVerifiedDomainEmails?: string[]; + suspendedAt?: string; }; // @public diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts index 2af60d2bb1..fc4fdb14da 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts @@ -1044,12 +1044,19 @@ describe('github', () => { }), ); - await getOrganizationUsers(graphql as any, org, 'token', undefined, { - teams: 10, - teamMembers: 20, - organizationMembers: 30, - repositories: 10, - }); + await getOrganizationUsers( + graphql as any, + org, + 'token', + false, + undefined, + { + teams: 10, + teamMembers: 20, + organizationMembers: 30, + repositories: 10, + }, + ); }); it('uses custom page sizes for getOrganizationRepositories', async () => { From 35c23e5463e03b67b028e6cca6f05f3847ce137f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A9rio=20Val=C3=A9rio?= Date: Tue, 7 Oct 2025 16:05:05 +0300 Subject: [PATCH 04/10] Update documentation to list the new option 'excludeSuspendedUsers' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Valério Valério --- docs/integrations/github/org.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index 6526183f2d..cfc71d3e5a 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -90,6 +90,7 @@ catalog: initialDelay: { seconds: 30 } frequency: { hours: 1 } timeout: { minutes: 50 } + excludeSuspendedUsers: true ``` Directly under the `githubOrg` is a list of configurations, each entry is a structure with the following elements: @@ -98,6 +99,7 @@ Directly under the `githubOrg` is a list of configurations, each entry is a stru - `githubUrl`: The target that this provider should consume - `orgs` (optional): The list of the GitHub orgs to consume. If you only list a single org the generated group entities will use the `default` namespace, otherwise they will use the org name as the namespace. By default the provider will consume all accessible orgs on the given GitHub instance (support for GitHub App integration only). - `schedule`: The refresh schedule to use, matches the structure of [`SchedulerServiceTaskScheduleDefinitionConfig`](https://backstage.io/docs/reference/backend-plugin-api.schedulerservicetaskscheduledefinitionconfig/) +<<<<<<< HEAD - `pageSizes` (optional): Configure page sizes for GitHub GraphQL API queries to prevent `RESOURCE_LIMITS_EXCEEDED` errors. You can configure the following page sizes: - `teams`: Number of teams to fetch per page when querying organization teams (default: 25) @@ -105,6 +107,9 @@ Directly under the `githubOrg` is a list of configurations, each entry is a stru - `organizationMembers`: Number of organization members to fetch per page (default: 50) Reducing page sizes will result in more API calls and slightly longer sync times, but will prevent API resource limits for organizations with large number of teams and members. +======= +- `excludeSuspendedUsers` (optional): Whether to exclude suspended users when querying organization users. Only for GitHub Enterprise instances. Will error if used against GitHub.com API. +>>>>>>> 40785ff87c (Update documentation to list the new option 'excludeSuspendedUsers') ### Events Support From ed5a7a3ef38a5e7d2c05b10899958ed92768c9db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A9rio=20Val=C3=A9rio?= Date: Tue, 7 Oct 2025 16:19:36 +0300 Subject: [PATCH 05/10] Add changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Valério Valério --- .changeset/fuzzy-phones-own.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/fuzzy-phones-own.md diff --git a/.changeset/fuzzy-phones-own.md b/.changeset/fuzzy-phones-own.md new file mode 100644 index 0000000000..a980bcffba --- /dev/null +++ b/.changeset/fuzzy-phones-own.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog-backend-module-github-org': minor +'@backstage/plugin-catalog-backend-module-github': minor +--- + +Introduce new configuration option to exclude suspended users from GitHub Enterprise instances. + +When it’s set to true, suspended users won’t be emitted by the default transform. +Note that this option should be used only against GitHub Enterprise instances, the property does not exist in the github.com GraphQL schema, setting it will cause a schema validation error and the syncing of users will fail. From bad559c1f9ae96382cd321d22b0c810e6eca4211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A9rio=20Val=C3=A9rio?= Date: Tue, 21 Oct 2025 13:31:17 +0300 Subject: [PATCH 06/10] Move the suspended user logic to a transformer filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Valério Valério --- .changeset/fuzzy-phones-own.md | 2 +- docs/integrations/github/org.md | 4 +--- plugins/catalog-backend-module-github/config.d.ts | 2 -- .../src/lib/defaultTransformers.ts | 3 --- .../src/lib/github.test.ts | 2 +- .../catalog-backend-module-github/src/lib/github.ts | 13 ++++++++++++- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/.changeset/fuzzy-phones-own.md b/.changeset/fuzzy-phones-own.md index a980bcffba..69a40e29b2 100644 --- a/.changeset/fuzzy-phones-own.md +++ b/.changeset/fuzzy-phones-own.md @@ -5,5 +5,5 @@ Introduce new configuration option to exclude suspended users from GitHub Enterprise instances. -When it’s set to true, suspended users won’t be emitted by the default transform. +When it’s set to true, suspended users won’t be returned when querying the organization users for GitHub Enterprise instances. Note that this option should be used only against GitHub Enterprise instances, the property does not exist in the github.com GraphQL schema, setting it will cause a schema validation error and the syncing of users will fail. diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index cfc71d3e5a..284451af8c 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -99,7 +99,6 @@ Directly under the `githubOrg` is a list of configurations, each entry is a stru - `githubUrl`: The target that this provider should consume - `orgs` (optional): The list of the GitHub orgs to consume. If you only list a single org the generated group entities will use the `default` namespace, otherwise they will use the org name as the namespace. By default the provider will consume all accessible orgs on the given GitHub instance (support for GitHub App integration only). - `schedule`: The refresh schedule to use, matches the structure of [`SchedulerServiceTaskScheduleDefinitionConfig`](https://backstage.io/docs/reference/backend-plugin-api.schedulerservicetaskscheduledefinitionconfig/) -<<<<<<< HEAD - `pageSizes` (optional): Configure page sizes for GitHub GraphQL API queries to prevent `RESOURCE_LIMITS_EXCEEDED` errors. You can configure the following page sizes: - `teams`: Number of teams to fetch per page when querying organization teams (default: 25) @@ -107,9 +106,8 @@ Directly under the `githubOrg` is a list of configurations, each entry is a stru - `organizationMembers`: Number of organization members to fetch per page (default: 50) Reducing page sizes will result in more API calls and slightly longer sync times, but will prevent API resource limits for organizations with large number of teams and members. -======= + - `excludeSuspendedUsers` (optional): Whether to exclude suspended users when querying organization users. Only for GitHub Enterprise instances. Will error if used against GitHub.com API. ->>>>>>> 40785ff87c (Update documentation to list the new option 'excludeSuspendedUsers') ### Events Support diff --git a/plugins/catalog-backend-module-github/config.d.ts b/plugins/catalog-backend-module-github/config.d.ts index 6277f7aa2e..5425cae9d0 100644 --- a/plugins/catalog-backend-module-github/config.d.ts +++ b/plugins/catalog-backend-module-github/config.d.ts @@ -266,7 +266,6 @@ export interface Config { /** * (Optional) Only for GitHub Enterprise. Whether to exclude suspended users when querying organization users. - * If true, the defaultTransformer will not return suspended users. * Default: `false`. */ excludeSuspendedUsers?: boolean; @@ -324,7 +323,6 @@ export interface Config { /** * (Optional) Only for GitHub Enterprise. Whether to exclude suspended users when querying organization users. - * If true, the defaultTransformer will not return suspended users. * Default: `false`. */ excludeSuspendedUsers?: boolean; diff --git a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts index 02f5052ae1..6befd32056 100644 --- a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts +++ b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts @@ -62,9 +62,6 @@ export const defaultUserTransformer = async ( item: GithubUser, _ctx: TransformerContext, ): Promise => { - if (item.suspendedAt) { - return undefined; - } const entity: UserEntity = { apiVersion: 'backstage.io/v1alpha1', kind: 'User', diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts index fc4fdb14da..3cfb1b523a 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts @@ -385,7 +385,7 @@ describe('github', () => { graphql, 'a', 'token', - true, + false, customUserTransformer, ), ).resolves.toEqual(output); diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts index b614b4be64..7f2fd0e08b 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.ts @@ -210,6 +210,17 @@ export async function getOrganizationUsers( } }`; + // Transformer to filter out suspended users, only for GitHub Enterprise instances. + const suspendedUserFilteringTransformer = async ( + item: GithubUser, + ctx: TransformerContext, + ): Promise => { + if (excludeSuspendedUsers && item.suspendedAt) { + return undefined; + } + return userTransformer(item, ctx); + }; + // There is no user -> teams edge, so we leave the memberships empty for // now and let the team iteration handle it instead @@ -218,7 +229,7 @@ export async function getOrganizationUsers( query, org, r => r.organization?.membersWithRole, - userTransformer, + suspendedUserFilteringTransformer, { org, email: tokenType === 'token', From 0bc546a459966e7d6e01dece9fec0b2193d36269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A9rio=20Val=C3=A9rio?= Date: Thu, 20 Nov 2025 13:05:10 +0200 Subject: [PATCH 07/10] Reorder parameters after rebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since a new paramater was introduced in the same function(getOrganizationUsers) Signed-off-by: Valério Valério --- .../src/module.ts | 6 ++-- .../src/lib/github.test.ts | 33 ++++++------------- .../src/lib/github.ts | 3 +- .../GithubMultiOrgReaderProcessor.ts | 1 - .../providers/GithubMultiOrgEntityProvider.ts | 6 ++-- .../src/providers/GithubOrgEntityProvider.ts | 10 ++++-- 6 files changed, 25 insertions(+), 34 deletions(-) diff --git a/plugins/catalog-backend-module-github-org/src/module.ts b/plugins/catalog-backend-module-github-org/src/module.ts index e6179e07f1..26abede133 100644 --- a/plugins/catalog-backend-module-github-org/src/module.ts +++ b/plugins/catalog-backend-module-github-org/src/module.ts @@ -134,13 +134,13 @@ function readDefinitionsFromConfig(rootConfig: Config): Array<{ id: string; githubUrl: string; orgs?: string[]; - excludeSuspendedUsers?: boolean; schedule: SchedulerServiceTaskScheduleDefinition; pageSizes?: { teams?: number; teamMembers?: number; organizationMembers?: number; }; + excludeSuspendedUsers?: boolean; }> { const baseKey = 'catalog.providers.githubOrg'; const baseConfig = rootConfig.getOptional(baseKey); @@ -156,8 +156,6 @@ function readDefinitionsFromConfig(rootConfig: Config): Array<{ id: c.getString('id'), githubUrl: c.getString('githubUrl'), orgs: c.getOptionalStringArray('orgs'), - excludeSuspendedUsers: - c.getOptionalBoolean('excludeSuspendedUsers') ?? false, schedule: readSchedulerServiceTaskScheduleDefinitionFromConfig( c.getConfig('schedule'), ), @@ -170,5 +168,7 @@ function readDefinitionsFromConfig(rootConfig: Config): Array<{ ), } : undefined, + excludeSuspendedUsers: + c.getOptionalBoolean('excludeSuspendedUsers') ?? false, })); } diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts index 3cfb1b523a..6933c9a245 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts @@ -211,7 +211,7 @@ describe('github', () => { ); await expect( - getOrganizationUsers(graphql, 'a', 'token', true), + getOrganizationUsers(graphql, 'a', 'token', undefined, undefined, true), ).resolves.toEqual(output); }); }); @@ -274,13 +274,7 @@ describe('github', () => { ); await expect( - getOrganizationUsers( - graphql, - 'a', - 'token', - false, - customUserTransformer, - ), + getOrganizationUsers(graphql, 'a', 'token', customUserTransformer), ).resolves.toEqual(output); }); @@ -327,7 +321,6 @@ describe('github', () => { graphql, 'a', 'token', - false, customUserTransformer, ); @@ -385,8 +378,9 @@ describe('github', () => { graphql, 'a', 'token', - false, customUserTransformer, + undefined, + false, ), ).resolves.toEqual(output); }); @@ -1044,19 +1038,12 @@ describe('github', () => { }), ); - await getOrganizationUsers( - graphql as any, - org, - 'token', - false, - undefined, - { - teams: 10, - teamMembers: 20, - organizationMembers: 30, - repositories: 10, - }, - ); + await getOrganizationUsers(graphql as any, org, 'token', undefined, { + teams: 10, + teamMembers: 20, + organizationMembers: 30, + repositories: 10, + }); }); it('uses custom page sizes for getOrganizationRepositories', async () => { diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts index 7f2fd0e08b..030fa93fad 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.ts @@ -182,14 +182,15 @@ export type Connection = { * @param tokenType - The type of GitHub credential * @param userTransformer - Optional transformer for user entities * @param pageSizes - Optional page sizes configuration + * @param excludeSuspendedUsers - Optional flag to exclude suspended users (only for GitHub Enterprise instances) */ export async function getOrganizationUsers( client: typeof graphql, org: string, tokenType: GithubCredentialType, - excludeSuspendedUsers: boolean = false, userTransformer: UserTransformer = defaultUserTransformer, pageSizes: GithubPageSizes = DEFAULT_PAGE_SIZES, + excludeSuspendedUsers: boolean = false, ): Promise<{ users: Entity[] }> { const suspendedAtField = excludeSuspendedUsers ? 'suspendedAt,' : ''; const query = ` diff --git a/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts b/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts index 058ce2cb6c..8af528dfcf 100644 --- a/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts @@ -148,7 +148,6 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { client, orgConfig.name, tokenType, - false, async (githubUser, ctx): Promise => { const result = this.options.userTransformer ? await this.options.userTransformer(githubUser, ctx) diff --git a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts index bb0dd84c6f..173da84f58 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts @@ -314,9 +314,9 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { client, org, tokenType, - this.options.excludeSuspendedUsers, this.options.userTransformer, pageSizes, + this.options.excludeSuspendedUsers, ); const { teams } = await getOrganizationTeams( @@ -467,9 +467,9 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { client, org, tokenType, - this.options.excludeSuspendedUsers, this.options.userTransformer, pageSizes, + this.options.excludeSuspendedUsers, ); const { teams } = await getOrganizationTeams( @@ -702,9 +702,9 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { client, org, tokenType, - this.options.excludeSuspendedUsers, this.options.userTransformer, pageSizes, + this.options.excludeSuspendedUsers, ); const usersFromChangedGroup = isGroupEntity(team) diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts index 31b6dc338b..4758311e83 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts @@ -53,6 +53,7 @@ import { createGraphqlClient, createRemoveEntitiesOperation, createReplaceEntitiesOperation, + DEFAULT_PAGE_SIZES, DeferredEntitiesBuilder, getOrganizationTeam, getOrganizationTeams, @@ -245,8 +246,9 @@ export class GithubOrgEntityProvider implements EntityProvider { client, org, tokenType, - this.options.excludeSuspendedUsers, this.options.userTransformer, + DEFAULT_PAGE_SIZES, + this.options.excludeSuspendedUsers, ); const { teams } = await getOrganizationTeams( client, @@ -374,8 +376,9 @@ export class GithubOrgEntityProvider implements EntityProvider { client, org, tokenType, - this.options.excludeSuspendedUsers, this.options.userTransformer, + DEFAULT_PAGE_SIZES, + this.options.excludeSuspendedUsers, ); if (!isGroupEntity(team)) { @@ -466,8 +469,9 @@ export class GithubOrgEntityProvider implements EntityProvider { client, org, tokenType, - this.options.excludeSuspendedUsers, this.options.userTransformer, + DEFAULT_PAGE_SIZES, + this.options.excludeSuspendedUsers, ); const usersToRebuild = users.filter(u => u.metadata.name === userLogin); From 268b0f99549f637b665943c1e8731426f7b942f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A9rio=20Val=C3=A9rio?= Date: Wed, 26 Nov 2025 09:46:40 +0200 Subject: [PATCH 08/10] Introduce a new optional filter in queryWithPaging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new filter will allow excluding items returned from the GitHub API, given a boolean condition, before passing them to the transformer. Signed-off-by: Valério Valério --- .../src/lib/github.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts index 030fa93fad..8d8410b182 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.ts @@ -211,17 +211,6 @@ export async function getOrganizationUsers( } }`; - // Transformer to filter out suspended users, only for GitHub Enterprise instances. - const suspendedUserFilteringTransformer = async ( - item: GithubUser, - ctx: TransformerContext, - ): Promise => { - if (excludeSuspendedUsers && item.suspendedAt) { - return undefined; - } - return userTransformer(item, ctx); - }; - // There is no user -> teams edge, so we leave the memberships empty for // now and let the team iteration handle it instead @@ -230,12 +219,13 @@ export async function getOrganizationUsers( query, org, r => r.organization?.membersWithRole, - suspendedUserFilteringTransformer, + userTransformer, { org, email: tokenType === 'token', organizationMembersPageSize: pageSizes.organizationMembers, }, + u => (excludeSuspendedUsers ? !u.suspendedAt : true), ); return { users }; @@ -772,6 +762,7 @@ export async function getTeamMembers( * @param transformer - A function that, given one of the nodes in the Connection, * returns the model mapped form of it * @param variables - The variable values that the query needs, minus the cursor + * @param filter - An optional filter function to filter the nodes before transforming them */ export async function queryWithPaging< GraphqlType, @@ -788,6 +779,7 @@ export async function queryWithPaging< ctx: TransformerContext, ) => Promise, variables: Variables, + filter?: (item: GraphqlType) => boolean, ): Promise { const result: OutputType[] = []; const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); @@ -805,6 +797,9 @@ export async function queryWithPaging< } for (const node of conn.nodes) { + if (filter && !filter(node)) { + continue; + } const transformedNode = await transformer(node, { client, query, From d1c7973084b3e4eef6a0bc6a2670fef62fb98c50 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 27 Nov 2025 11:43:11 +0100 Subject: [PATCH 09/10] chore: small refactor Signed-off-by: benjdlambert --- .../src/lib/github.ts | 113 ++++++++++-------- 1 file changed, 60 insertions(+), 53 deletions(-) diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts index 8d8410b182..bd77df4da7 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.ts @@ -214,19 +214,19 @@ export async function getOrganizationUsers( // There is no user -> teams edge, so we leave the memberships empty for // now and let the team iteration handle it instead - const users = await queryWithPaging( + const users = await queryWithPaging({ client, query, org, - r => r.organization?.membersWithRole, - userTransformer, - { + connection: r => r.organization?.membersWithRole, + transformer: userTransformer, + variables: { org, email: tokenType === 'token', organizationMembersPageSize: pageSizes.organizationMembers, }, - u => (excludeSuspendedUsers ? !u.suspendedAt : true), - ); + filter: u => (excludeSuspendedUsers ? !u.suspendedAt : true), + }); return { users }; } @@ -311,18 +311,18 @@ export async function getOrganizationTeams( return await teamTransformer(team, ctx); }; - const teams = await queryWithPaging( + const teams = await queryWithPaging({ client, query, org, - r => r.organization?.teams, - materialisedTeams, - { + connection: r => r.organization?.teams, + transformer: materialisedTeams, + variables: { org, teamsPageSize: pageSizes.teams, membersPageSize: pageSizes.teamMembers, }, - ); + }); return { teams }; } @@ -405,19 +405,19 @@ export async function getOrganizationTeamsFromUsers( return await teamTransformer(team, ctx); }; - const teams = await queryWithPaging( + const teams = await queryWithPaging({ client, query, org, - r => r.organization?.teams, - materialisedTeams, - { + connection: r => r.organization?.teams, + transformer: materialisedTeams, + variables: { org, userLogins, teamsPageSize: pageSizes.teams, membersPageSize: pageSizes.teamMembers, }, - ); + }); return { teams }; } @@ -464,14 +464,14 @@ export async function getOrganizationTeamsForUser( return await teamTransformer(team, ctx); }; - const teams = await queryWithPaging( + const teams = await queryWithPaging({ client, query, org, - r => r.organization?.teams, - materialisedTeams, - { org, userLogins: [userLogin], teamsPageSize: pageSizes.teams }, - ); + connection: r => r.organization?.teams, + transformer: materialisedTeams, + variables: { org, userLogins: [userLogin], teamsPageSize: pageSizes.teams }, + }); return { teams }; } @@ -492,14 +492,14 @@ export async function getOrganizationsFromUser( } }`; - const orgs = await queryWithPaging( + const orgs = await queryWithPaging({ client, query, - '', - r => r.user?.organizations, - async o => o.login, - { user }, - ); + org: '', + connection: r => r.user?.organizations, + transformer: async o => o.login, + variables: { user }, + }); return { orgs }; } @@ -638,14 +638,18 @@ export async function getOrganizationRepositories( } }`; - const repositories = await queryWithPaging( + const repositories = await queryWithPaging({ client, query, org, - r => r.repositoryOwner?.repositories, - async x => x, - { org, catalogPathRef, repositoriesPageSize: pageSizes.repositories }, - ); + connection: r => r.repositoryOwner?.repositories, + transformer: async x => x, + variables: { + org, + catalogPathRef, + repositoriesPageSize: pageSizes.repositories, + }, + }); return { repositories }; } @@ -733,14 +737,14 @@ export async function getTeamMembers( } }`; - const members = await queryWithPaging( + const members = await queryWithPaging({ client, query, org, - r => r.organization?.team?.members, - async user => user, - { org, teamSlug, membersPageSize: pageSizes.teamMembers }, - ); + connection: r => r.organization?.team?.members, + transformer: async user => user, + variables: { org, teamSlug, membersPageSize: pageSizes.teamMembers }, + }); return { members }; } @@ -754,33 +758,36 @@ export async function getTeamMembers( * * Requires that the query accepts a $cursor variable. * - * @param client - The octokit client - * @param query - The query to execute - * @param org - The slug of the org to read - * @param connection - A function that, given the response, picks out the actual + * @param params - Object containing all parameters + * @param params.client - The octokit client + * @param params.query - The query to execute + * @param params.org - The slug of the org to read + * @param params.connection - A function that, given the response, picks out the actual * Connection object that's being iterated - * @param transformer - A function that, given one of the nodes in the Connection, + * @param params.transformer - A function that, given one of the nodes in the Connection, * returns the model mapped form of it - * @param variables - The variable values that the query needs, minus the cursor - * @param filter - An optional filter function to filter the nodes before transforming them + * @param params.variables - The variable values that the query needs, minus the cursor + * @param params.filter - An optional filter function to filter the nodes before transforming them */ export async function queryWithPaging< GraphqlType, OutputType, Variables extends {}, Response = QueryResponse, ->( - client: typeof graphql, - query: string, - org: string, - connection: (response: Response) => Connection | undefined, +>(params: { + client: typeof graphql; + query: string; + org: string; + connection: (response: Response) => Connection | undefined; transformer: ( item: GraphqlType, ctx: TransformerContext, - ) => Promise, - variables: Variables, - filter?: (item: GraphqlType) => boolean, -): Promise { + ) => Promise; + variables: Variables; + filter?: (item: GraphqlType) => boolean; +}): Promise { + const { client, query, org, connection, transformer, variables, filter } = + params; const result: OutputType[] = []; const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); From 459df27cc746f59cce381053d84850df6f51cfd3 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Tue, 2 Dec 2025 11:02:59 +0100 Subject: [PATCH 10/10] Update versioning for GitHub plugins to patch Introduce a configuration option to exclude suspended users from GitHub Enterprise instances. Signed-off-by: Ben Lambert --- .changeset/fuzzy-phones-own.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/fuzzy-phones-own.md b/.changeset/fuzzy-phones-own.md index 69a40e29b2..61725d1f1d 100644 --- a/.changeset/fuzzy-phones-own.md +++ b/.changeset/fuzzy-phones-own.md @@ -1,6 +1,6 @@ --- -'@backstage/plugin-catalog-backend-module-github-org': minor -'@backstage/plugin-catalog-backend-module-github': minor +'@backstage/plugin-catalog-backend-module-github-org': patch +'@backstage/plugin-catalog-backend-module-github': patch --- Introduce new configuration option to exclude suspended users from GitHub Enterprise instances.