feat(catalog-backend-module-github): prefer verified org emails in default user transformer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: djamaile <rdjamaile@gmail.com>
This commit is contained in:
djamaile
2026-02-25 10:57:05 +01:00
committed by djamailer
parent b42fcdca2e
commit b11c2cd8e2
3 changed files with 171 additions and 1 deletions
+34
View File
@@ -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;
});
},
});
},
});
```
@@ -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> = {}): 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();
});
});
@@ -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;
};