From b11c2cd8e21dfbf0db4976f9a973fdf53eccc402 Mon Sep 17 00:00:00 2001 From: djamaile Date: Wed, 25 Feb 2026 10:57:05 +0100 Subject: [PATCH] feat(catalog-backend-module-github): prefer verified org emails in default user transformer Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: djamaile --- .changeset/github-org-verified-emails.md | 34 +++++ .../src/lib/defaultTransformers.test.ts | 131 ++++++++++++++++++ .../src/lib/defaultTransformers.ts | 7 +- 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 .changeset/github-org-verified-emails.md create mode 100644 plugins/catalog-backend-module-github/src/lib/defaultTransformers.test.ts diff --git a/.changeset/github-org-verified-emails.md b/.changeset/github-org-verified-emails.md new file mode 100644 index 0000000000..b825395556 --- /dev/null +++ b/.changeset/github-org-verified-emails.md @@ -0,0 +1,34 @@ +--- +'@backstage/plugin-catalog-backend-module-github': minor +--- + +The default user transformer now prefers organization verified domain emails over the user's public GitHub email when populating the user entity profile. It also strips plus-addressed routing tags that GitHub adds to these emails. + +If you want to retain the old behavior, you can do so with a custom user transformer using the `githubOrgEntityProviderTransformsExtensionPoint`: + +```ts +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { githubOrgEntityProviderTransformsExtensionPoint } from '@backstage/plugin-catalog-backend-module-github-org'; +import { defaultUserTransformer } from '@backstage/plugin-catalog-backend-module-github'; + +export default createBackendModule({ + pluginId: 'catalog', + moduleId: 'github-org-custom-transforms', + register(env) { + env.registerInit({ + deps: { + transforms: githubOrgEntityProviderTransformsExtensionPoint, + }, + async init({ transforms }) { + transforms.setUserTransformer(async (item, ctx) => { + const entity = await defaultUserTransformer(item, ctx); + if (entity && item.email) { + entity.spec.profile!.email = item.email; + } + return entity; + }); + }, + }); + }, +}); +``` diff --git a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.test.ts b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.test.ts new file mode 100644 index 0000000000..f90d67bcc9 --- /dev/null +++ b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.test.ts @@ -0,0 +1,131 @@ +/* + * 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 { UserEntity } from '@backstage/catalog-model'; +import { graphql } from '@octokit/graphql'; +import { + defaultUserTransformer, + TransformerContext, +} from './defaultTransformers'; +import { GithubUser } from './github'; + +const ctx: TransformerContext = { + client: graphql, + query: '', + org: 'test-org', +}; + +function makeUser(overrides: Partial = {}): GithubUser { + return { + login: 'testuser', + avatarUrl: '', + ...overrides, + }; +} + +describe('defaultUserTransformer', () => { + it('populates all fields correctly', async () => { + const result = (await defaultUserTransformer( + makeUser({ + name: 'Test User', + email: 'test@example.com', + bio: 'A test bio', + avatarUrl: 'https://example.com/avatar.png', + id: 'user-id-123', + }), + ctx, + )) as UserEntity; + + expect(result.metadata.name).toBe('testuser'); + expect(result.metadata.description).toBe('A test bio'); + expect(result.metadata.annotations).toEqual({ + 'github.com/user-login': 'testuser', + 'github.com/user-id': 'user-id-123', + }); + expect(result.spec.profile).toEqual({ + displayName: 'Test User', + email: 'test@example.com', + picture: 'https://example.com/avatar.png', + }); + expect(result.spec.memberOf).toEqual([]); + }); + + it('prefers verified domain email over regular email', async () => { + const result = (await defaultUserTransformer( + makeUser({ + email: 'public@gmail.com', + organizationVerifiedDomainEmails: ['corp@company.com'], + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBe('corp@company.com'); + }); + + it('strips plus-addressed tag from verified domain email', async () => { + const result = (await defaultUserTransformer( + makeUser({ + organizationVerifiedDomainEmails: ['amckay+2jc29kv2@spotify.com'], + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBe('amckay@spotify.com'); + }); + + it('uses verified domain email when regular email is absent', async () => { + const result = (await defaultUserTransformer( + makeUser({ + organizationVerifiedDomainEmails: ['corp@company.com'], + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBe('corp@company.com'); + }); + + it('falls back to regular email when verified array is empty', async () => { + const result = (await defaultUserTransformer( + makeUser({ + email: 'public@gmail.com', + organizationVerifiedDomainEmails: [], + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBe('public@gmail.com'); + }); + + it('falls back to regular email when verified array is undefined', async () => { + const result = (await defaultUserTransformer( + makeUser({ + email: 'public@gmail.com', + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBe('public@gmail.com'); + }); + + it('sets no email when both are absent', async () => { + const result = (await defaultUserTransformer( + makeUser(), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBeUndefined(); + }); +}); diff --git a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts index 4de9af89c8..d71cda270d 100644 --- a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts +++ b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts @@ -81,7 +81,12 @@ export const defaultUserTransformer = async ( if (item.bio) entity.metadata.description = item.bio; if (item.name) entity.spec.profile!.displayName = item.name; - if (item.email) entity.spec.profile!.email = item.email; + // GitHub returns verified domain emails as plus-addressed routing aliases + // (e.g. user+abc123@example.com). Strip the tag to get the real address. + const email = item.organizationVerifiedDomainEmails?.length + ? item.organizationVerifiedDomainEmails[0].replace(/\+[^@]*/, '') + : item.email; + if (email) entity.spec.profile!.email = email; if (item.avatarUrl) entity.spec.profile!.picture = item.avatarUrl; return entity; };