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:
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user