refct(events,catalog/github): change membership and team.edit handlers
Signed-off-by: Rogerio Angeliski <angeliski@hotmail.com>
This commit is contained in:
@@ -246,7 +246,6 @@ export class GithubOrgReaderProcessor implements CatalogProcessor {
|
||||
|
||||
// @public
|
||||
export type GithubTeam = {
|
||||
databaseId: number;
|
||||
slug: string;
|
||||
combinedSlug: string;
|
||||
name?: string;
|
||||
|
||||
@@ -90,10 +90,6 @@ export const defaultOrganizationTeamTransformer: TeamTransformer =
|
||||
'github.com/team-slug': team.combinedSlug,
|
||||
};
|
||||
|
||||
if (team.databaseId) {
|
||||
annotations['github.com/team-id'] = `${team.databaseId}`;
|
||||
}
|
||||
|
||||
if (team.editTeamUrl) {
|
||||
annotations['backstage.io/edit-url'] = team.editTeamUrl;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
GithubTeam,
|
||||
createAddEntitiesOperation,
|
||||
createRemoveEntitiesOperation,
|
||||
createReplaceEntitiesOperation,
|
||||
} from './github';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
@@ -206,7 +207,6 @@ describe('github', () => {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [
|
||||
{
|
||||
databaseId: 1,
|
||||
slug: 'team',
|
||||
combinedSlug: 'blah/team',
|
||||
name: 'Team',
|
||||
@@ -214,7 +214,6 @@ describe('github', () => {
|
||||
avatarUrl: 'http://example.com/team.jpeg',
|
||||
editTeamUrl: 'http://example.com/orgs/blah/teams/team/edit',
|
||||
parentTeam: {
|
||||
databaseId: 2,
|
||||
slug: 'parent',
|
||||
combinedSlug: '',
|
||||
members: [],
|
||||
@@ -239,7 +238,6 @@ describe('github', () => {
|
||||
description: 'The one and only team',
|
||||
annotations: {
|
||||
'github.com/team-slug': 'blah/team',
|
||||
'github.com/team-id': '1',
|
||||
'backstage.io/edit-url':
|
||||
'http://example.com/orgs/blah/teams/team/edit',
|
||||
},
|
||||
@@ -309,7 +307,6 @@ describe('github', () => {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [
|
||||
{
|
||||
databaseId: 1,
|
||||
slug: 'team',
|
||||
combinedSlug: 'blah/team',
|
||||
name: 'Team',
|
||||
@@ -317,7 +314,6 @@ describe('github', () => {
|
||||
avatarUrl: 'http://example.com/team.jpeg',
|
||||
editTeamUrl: 'http://example.com/orgs/blah/teams/team/edit',
|
||||
parentTeam: {
|
||||
databaseId: 3,
|
||||
slug: 'parent',
|
||||
combinedSlug: '',
|
||||
members: [],
|
||||
@@ -376,7 +372,6 @@ describe('github', () => {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [
|
||||
{
|
||||
databaseId: 1,
|
||||
slug: 'team',
|
||||
combinedSlug: 'blah/team',
|
||||
name: 'Team',
|
||||
@@ -384,7 +379,6 @@ describe('github', () => {
|
||||
avatarUrl: 'http://example.com/team.jpeg',
|
||||
editTeamUrl: 'http://example.com/orgs/blah/teams/team/edit',
|
||||
parentTeam: {
|
||||
databaseId: 3,
|
||||
slug: 'parent',
|
||||
combinedSlug: '',
|
||||
members: [],
|
||||
@@ -395,7 +389,6 @@ describe('github', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
databaseId: 2,
|
||||
slug: 'team',
|
||||
combinedSlug: 'blah/team',
|
||||
name: 'aa',
|
||||
@@ -403,7 +396,6 @@ describe('github', () => {
|
||||
avatarUrl: 'http://example.com/team.jpeg',
|
||||
editTeamUrl: 'http://example.com/orgs/blah/teams/team/edit',
|
||||
parentTeam: {
|
||||
databaseId: 3,
|
||||
slug: 'parent',
|
||||
combinedSlug: '',
|
||||
members: [],
|
||||
@@ -464,7 +456,6 @@ describe('github', () => {
|
||||
const input: QueryResponse = {
|
||||
organization: {
|
||||
team: {
|
||||
databaseId: 1,
|
||||
slug: '',
|
||||
combinedSlug: '',
|
||||
members: {
|
||||
@@ -635,4 +626,41 @@ describe('github', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('createReplaceEntitiesOperation', () => {
|
||||
it('create a function to replace deferred entities to a delta operation', () => {
|
||||
const operation = createReplaceEntitiesOperation('my-id', 'host');
|
||||
|
||||
const userEntity: UserEntity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'githubuser',
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://github.com/githubuser',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://github.com/githubuser',
|
||||
'github.com/user-login': 'githubuser',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
memberOf: ['new-team'],
|
||||
},
|
||||
};
|
||||
expect(operation('org', [userEntity])).toEqual({
|
||||
removed: [
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: userEntity,
|
||||
},
|
||||
],
|
||||
added: [
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: userEntity,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,7 +71,6 @@ export type GithubUser = {
|
||||
* @public
|
||||
*/
|
||||
export type GithubTeam = {
|
||||
databaseId: number;
|
||||
slug: string;
|
||||
combinedSlug: string;
|
||||
name?: string;
|
||||
@@ -186,7 +185,6 @@ export async function getOrganizationTeams(
|
||||
teams(first: 100, after: $cursor) {
|
||||
pageInfo { hasNextPage, endCursor }
|
||||
nodes {
|
||||
databaseId
|
||||
slug
|
||||
combinedSlug
|
||||
name
|
||||
@@ -196,7 +194,14 @@ export async function getOrganizationTeams(
|
||||
parentTeam { slug }
|
||||
members(first: 100, membership: IMMEDIATE) {
|
||||
pageInfo { hasNextPage }
|
||||
nodes { login }
|
||||
nodes {
|
||||
avatarUrl,
|
||||
bio,
|
||||
email,
|
||||
login,
|
||||
name,
|
||||
organizationVerifiedDomainEmails(login: $org)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,6 +248,164 @@ export async function getOrganizationTeams(
|
||||
return { groups };
|
||||
}
|
||||
|
||||
export async function getOrganizationTeamsFromUsers(
|
||||
client: typeof graphql,
|
||||
org: string,
|
||||
userLogins: string[],
|
||||
teamTransformer: TeamTransformer = defaultOrganizationTeamTransformer,
|
||||
): Promise<{
|
||||
groups: GroupEntity[];
|
||||
}> {
|
||||
const query = `
|
||||
query teams($org: String!, $cursor: String, $userLogins: [String!] = "") {
|
||||
organization(login: $org) {
|
||||
teams(first: 100, after: $cursor, userLogins: $userLogins) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
nodes {
|
||||
slug
|
||||
combinedSlug
|
||||
name
|
||||
description
|
||||
avatarUrl
|
||||
editTeamUrl
|
||||
parentTeam {
|
||||
slug
|
||||
}
|
||||
members(first: 100, membership: IMMEDIATE) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
nodes {
|
||||
avatarUrl,
|
||||
bio,
|
||||
email,
|
||||
login,
|
||||
name,
|
||||
organizationVerifiedDomainEmails(login: $org)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const materialisedTeams = async (
|
||||
item: GithubTeamResponse,
|
||||
ctx: TransformerContext,
|
||||
): Promise<GroupEntity | undefined> => {
|
||||
const memberNames: GithubUser[] = [];
|
||||
|
||||
if (!item.members.pageInfo.hasNextPage) {
|
||||
// We got all the members in one go, run the fast path
|
||||
for (const user of item.members.nodes) {
|
||||
memberNames.push(user);
|
||||
}
|
||||
} else {
|
||||
// There were more than a hundred immediate members - run the slow
|
||||
// path of fetching them explicitly
|
||||
const { members } = await getTeamMembers(ctx.client, ctx.org, item.slug);
|
||||
for (const userLogin of members) {
|
||||
memberNames.push(userLogin);
|
||||
}
|
||||
}
|
||||
|
||||
const team: GithubTeam = {
|
||||
...item,
|
||||
members: memberNames,
|
||||
};
|
||||
|
||||
return await teamTransformer(team, ctx);
|
||||
};
|
||||
|
||||
const groups = await queryWithPaging(
|
||||
client,
|
||||
query,
|
||||
org,
|
||||
r => r.organization?.teams,
|
||||
materialisedTeams,
|
||||
{ org, userLogins },
|
||||
);
|
||||
|
||||
return { groups };
|
||||
}
|
||||
|
||||
export async function getOrganizationTeam(
|
||||
client: typeof graphql,
|
||||
org: string,
|
||||
teamSlug: string,
|
||||
teamTransformer: TeamTransformer = defaultOrganizationTeamTransformer,
|
||||
): Promise<{
|
||||
group: GroupEntity;
|
||||
}> {
|
||||
const query = `
|
||||
query teams($org: String!, $teamSlug: String!) {
|
||||
organization(login: $org) {
|
||||
team(slug:$teamSlug) {
|
||||
slug
|
||||
combinedSlug
|
||||
name
|
||||
description
|
||||
avatarUrl
|
||||
editTeamUrl
|
||||
parentTeam { slug }
|
||||
members(first: 100, membership: IMMEDIATE) {
|
||||
pageInfo { hasNextPage }
|
||||
nodes { login }
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const materialisedTeam = async (
|
||||
item: GithubTeamResponse,
|
||||
ctx: TransformerContext,
|
||||
): Promise<GroupEntity | undefined> => {
|
||||
const memberNames: GithubUser[] = [];
|
||||
|
||||
if (!item.members.pageInfo.hasNextPage) {
|
||||
// We got all the members in one go, run the fast path
|
||||
for (const user of item.members.nodes) {
|
||||
memberNames.push(user);
|
||||
}
|
||||
} else {
|
||||
// There were more than a hundred immediate members - run the slow
|
||||
// path of fetching them explicitly
|
||||
const { members } = await getTeamMembers(ctx.client, ctx.org, item.slug);
|
||||
for (const userLogin of members) {
|
||||
memberNames.push(userLogin);
|
||||
}
|
||||
}
|
||||
|
||||
const team: GithubTeam = {
|
||||
...item,
|
||||
members: memberNames,
|
||||
};
|
||||
|
||||
return await teamTransformer(team, ctx);
|
||||
};
|
||||
|
||||
const response: QueryResponse = await client(query, {
|
||||
org,
|
||||
teamSlug,
|
||||
});
|
||||
|
||||
if (!response.organization?.team)
|
||||
throw new Error(`Found no match for group ${teamSlug}`);
|
||||
|
||||
const group = await materialisedTeam(response.organization?.team, {
|
||||
query,
|
||||
client,
|
||||
org,
|
||||
});
|
||||
|
||||
if (!group) throw new Error(`Can't transform for group ${teamSlug}`);
|
||||
|
||||
return { group };
|
||||
}
|
||||
|
||||
export async function getOrganizationRepositories(
|
||||
client: typeof graphql,
|
||||
org: string,
|
||||
@@ -435,3 +598,16 @@ export const createRemoveEntitiesOperation =
|
||||
entity: withLocations(`https://${host}`, org, entity),
|
||||
})),
|
||||
});
|
||||
|
||||
export const createReplaceEntitiesOperation =
|
||||
(id: string, host: string) => (org: string, entities: Entity[]) => {
|
||||
const entitiesToReplace = entities.map(entity => ({
|
||||
locationKey: `github-org-provider:${id}`,
|
||||
entity: withLocations(`https://${host}`, org, entity),
|
||||
}));
|
||||
|
||||
return {
|
||||
removed: entitiesToReplace,
|
||||
added: entitiesToReplace,
|
||||
};
|
||||
};
|
||||
|
||||
+297
-74
@@ -628,6 +628,26 @@ describe('GithubOrgEntityProvider', () => {
|
||||
const mockClient = jest.fn();
|
||||
|
||||
mockClient
|
||||
.mockResolvedValueOnce({
|
||||
organization: {
|
||||
team: {
|
||||
slug: 'team',
|
||||
combinedSlug: 'blah/team',
|
||||
name: 'Team',
|
||||
description: 'The one and only team',
|
||||
avatarUrl: 'http://example.com/team.jpeg',
|
||||
parentTeam: {
|
||||
slug: 'parent',
|
||||
combinedSlug: '',
|
||||
members: { pageInfo: { hasNextPage: false }, nodes: [] },
|
||||
},
|
||||
members: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [{ login: 'a' }, { login: 'githubuser' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
organization: {
|
||||
membersWithRole: {
|
||||
@@ -640,6 +660,13 @@ describe('GithubOrgEntityProvider', () => {
|
||||
email: 'd',
|
||||
avatarUrl: 'e',
|
||||
},
|
||||
{
|
||||
login: 'githubuser',
|
||||
name: 'githubuser',
|
||||
bio: 'githubuser',
|
||||
email: 'd',
|
||||
avatarUrl: 'e',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -662,7 +689,7 @@ describe('GithubOrgEntityProvider', () => {
|
||||
},
|
||||
members: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [{ login: 'a' }],
|
||||
nodes: [{ login: 'a' }, { login: 'githubuser' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -680,7 +707,7 @@ describe('GithubOrgEntityProvider', () => {
|
||||
action: 'edited',
|
||||
changes: {
|
||||
name: {
|
||||
from: 'mygroup',
|
||||
from: 'mygroup with spaces',
|
||||
},
|
||||
},
|
||||
team: {
|
||||
@@ -704,11 +731,10 @@ describe('GithubOrgEntityProvider', () => {
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
|
||||
entities: [
|
||||
added: [
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
@@ -717,24 +743,50 @@ describe('GithubOrgEntityProvider', () => {
|
||||
'url:https://github.com/a',
|
||||
'github.com/user-login': 'a',
|
||||
},
|
||||
description: 'c',
|
||||
name: 'a',
|
||||
description: 'c',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
spec: {
|
||||
memberOf: ['team'],
|
||||
profile: {
|
||||
displayName: 'b',
|
||||
email: 'd',
|
||||
picture: 'e',
|
||||
},
|
||||
memberOf: ['team'],
|
||||
},
|
||||
},
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
},
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: {
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://github.com/githubuser',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://github.com/githubuser',
|
||||
'github.com/user-login': 'githubuser',
|
||||
},
|
||||
name: 'githubuser',
|
||||
description: 'githubuser',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
kind: 'User',
|
||||
spec: {
|
||||
profile: {
|
||||
displayName: 'githubuser',
|
||||
email: 'd',
|
||||
picture: 'e',
|
||||
},
|
||||
memberOf: ['team'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: {
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
@@ -746,21 +798,50 @@ describe('GithubOrgEntityProvider', () => {
|
||||
name: 'team',
|
||||
description: 'The one and only team',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
spec: {
|
||||
children: [],
|
||||
parent: 'parent',
|
||||
type: 'team',
|
||||
profile: {
|
||||
displayName: 'Team',
|
||||
picture: 'http://example.com/team.jpeg',
|
||||
},
|
||||
type: 'team',
|
||||
members: ['a'],
|
||||
children: [],
|
||||
parent: 'parent',
|
||||
members: ['a', 'githubuser'],
|
||||
},
|
||||
},
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
},
|
||||
],
|
||||
type: 'full',
|
||||
removed: [
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: {
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://github.com/orgs/backstage/teams/mygroup-with-spaces',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://github.com/orgs/backstage/teams/mygroup-with-spaces',
|
||||
},
|
||||
name: 'mygroup-with-spaces',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
spec: {
|
||||
type: 'team',
|
||||
profile: {
|
||||
displayName: 'Team',
|
||||
picture: 'http://example.com/team.jpeg',
|
||||
},
|
||||
children: [],
|
||||
parent: 'parent',
|
||||
members: ['a', 'githubuser'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'delta',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -795,6 +876,26 @@ describe('GithubOrgEntityProvider', () => {
|
||||
const mockClient = jest.fn();
|
||||
|
||||
mockClient
|
||||
.mockResolvedValueOnce({
|
||||
organization: {
|
||||
team: {
|
||||
slug: 'team',
|
||||
combinedSlug: 'blah/team',
|
||||
name: 'Team',
|
||||
description: 'The one and only team',
|
||||
avatarUrl: 'http://example.com/team.jpeg',
|
||||
parentTeam: {
|
||||
slug: 'parent',
|
||||
combinedSlug: '',
|
||||
members: { pageInfo: { hasNextPage: false }, nodes: [] },
|
||||
},
|
||||
members: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [{ login: 'a' }, { login: 'githubuser' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
organization: {
|
||||
membersWithRole: {
|
||||
@@ -807,6 +908,13 @@ describe('GithubOrgEntityProvider', () => {
|
||||
email: 'd',
|
||||
avatarUrl: 'e',
|
||||
},
|
||||
{
|
||||
login: 'githubuser',
|
||||
name: 'githubuser',
|
||||
bio: 'githubuser',
|
||||
email: 'd',
|
||||
avatarUrl: 'e',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -829,7 +937,7 @@ describe('GithubOrgEntityProvider', () => {
|
||||
},
|
||||
members: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [{ login: 'a' }],
|
||||
nodes: [{ login: 'a' }, { login: 'githubuser' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -867,37 +975,36 @@ describe('GithubOrgEntityProvider', () => {
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
|
||||
entities: [
|
||||
added: [
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://github.com/a',
|
||||
'url:https://github.com/githubuser',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://github.com/a',
|
||||
'github.com/user-login': 'a',
|
||||
'url:https://github.com/githubuser',
|
||||
'github.com/user-login': 'githubuser',
|
||||
},
|
||||
description: 'c',
|
||||
name: 'a',
|
||||
name: 'githubuser',
|
||||
description: 'githubuser',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
spec: {
|
||||
memberOf: ['team'],
|
||||
profile: {
|
||||
displayName: 'b',
|
||||
displayName: 'githubuser',
|
||||
email: 'd',
|
||||
picture: 'e',
|
||||
},
|
||||
memberOf: ['team'],
|
||||
},
|
||||
},
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
},
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
@@ -909,21 +1016,78 @@ describe('GithubOrgEntityProvider', () => {
|
||||
name: 'team',
|
||||
description: 'The one and only team',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
spec: {
|
||||
children: [],
|
||||
parent: 'parent',
|
||||
type: 'team',
|
||||
profile: {
|
||||
displayName: 'Team',
|
||||
picture: 'http://example.com/team.jpeg',
|
||||
},
|
||||
type: 'team',
|
||||
members: ['a'],
|
||||
children: [],
|
||||
parent: 'parent',
|
||||
members: ['a', 'githubuser'],
|
||||
},
|
||||
},
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
},
|
||||
],
|
||||
type: 'full',
|
||||
removed: [
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: {
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://github.com/githubuser',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://github.com/githubuser',
|
||||
'github.com/user-login': 'githubuser',
|
||||
},
|
||||
name: 'githubuser',
|
||||
description: 'githubuser',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
spec: {
|
||||
profile: {
|
||||
displayName: 'githubuser',
|
||||
email: 'd',
|
||||
picture: 'e',
|
||||
},
|
||||
memberOf: ['team'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: {
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://github.com/orgs/backstage/teams/team',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://github.com/orgs/backstage/teams/team',
|
||||
'github.com/team-slug': 'blah/team',
|
||||
},
|
||||
name: 'team',
|
||||
description: 'The one and only team',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
spec: {
|
||||
type: 'team',
|
||||
profile: {
|
||||
displayName: 'Team',
|
||||
picture: 'http://example.com/team.jpeg',
|
||||
},
|
||||
children: [],
|
||||
parent: 'parent',
|
||||
members: ['a', 'githubuser'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'delta',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -958,15 +1122,35 @@ describe('GithubOrgEntityProvider', () => {
|
||||
const mockClient = jest.fn();
|
||||
|
||||
mockClient
|
||||
.mockResolvedValueOnce({
|
||||
organization: {
|
||||
team: {
|
||||
slug: 'team',
|
||||
combinedSlug: 'blah/team',
|
||||
name: 'Team',
|
||||
description: 'The one and only team',
|
||||
avatarUrl: 'http://example.com/team.jpeg',
|
||||
parentTeam: {
|
||||
slug: 'parent',
|
||||
combinedSlug: '',
|
||||
members: { pageInfo: { hasNextPage: false }, nodes: [] },
|
||||
},
|
||||
members: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [{ login: 'a' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
organization: {
|
||||
membersWithRole: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [
|
||||
{
|
||||
login: 'a',
|
||||
name: 'b',
|
||||
bio: 'c',
|
||||
login: 'githubuser',
|
||||
name: 'githubuser',
|
||||
bio: 'githubuser',
|
||||
email: 'd',
|
||||
avatarUrl: 'e',
|
||||
},
|
||||
@@ -978,24 +1162,7 @@ describe('GithubOrgEntityProvider', () => {
|
||||
organization: {
|
||||
teams: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [
|
||||
{
|
||||
slug: 'team',
|
||||
combinedSlug: 'blah/team',
|
||||
name: 'Team',
|
||||
description: 'The one and only team',
|
||||
avatarUrl: 'http://example.com/team.jpeg',
|
||||
parentTeam: {
|
||||
slug: 'parent',
|
||||
combinedSlug: '',
|
||||
members: { pageInfo: { hasNextPage: false }, nodes: [] },
|
||||
},
|
||||
members: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [{ login: 'a' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1030,37 +1197,36 @@ describe('GithubOrgEntityProvider', () => {
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
|
||||
entities: [
|
||||
added: [
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://github.com/a',
|
||||
'url:https://github.com/githubuser',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://github.com/a',
|
||||
'github.com/user-login': 'a',
|
||||
'url:https://github.com/githubuser',
|
||||
'github.com/user-login': 'githubuser',
|
||||
},
|
||||
description: 'c',
|
||||
name: 'a',
|
||||
name: 'githubuser',
|
||||
description: 'githubuser',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
spec: {
|
||||
memberOf: ['team'],
|
||||
profile: {
|
||||
displayName: 'b',
|
||||
displayName: 'githubuser',
|
||||
email: 'd',
|
||||
picture: 'e',
|
||||
},
|
||||
memberOf: [],
|
||||
},
|
||||
},
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
},
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
@@ -1072,21 +1238,78 @@ describe('GithubOrgEntityProvider', () => {
|
||||
name: 'team',
|
||||
description: 'The one and only team',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
spec: {
|
||||
children: [],
|
||||
parent: 'parent',
|
||||
type: 'team',
|
||||
profile: {
|
||||
displayName: 'Team',
|
||||
picture: 'http://example.com/team.jpeg',
|
||||
},
|
||||
type: 'team',
|
||||
children: [],
|
||||
parent: 'parent',
|
||||
members: ['a'],
|
||||
},
|
||||
},
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
},
|
||||
],
|
||||
type: 'full',
|
||||
removed: [
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: {
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://github.com/githubuser',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://github.com/githubuser',
|
||||
'github.com/user-login': 'githubuser',
|
||||
},
|
||||
name: 'githubuser',
|
||||
description: 'githubuser',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
spec: {
|
||||
profile: {
|
||||
displayName: 'githubuser',
|
||||
email: 'd',
|
||||
picture: 'e',
|
||||
},
|
||||
memberOf: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
locationKey: 'github-org-provider:my-id',
|
||||
entity: {
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://github.com/orgs/backstage/teams/team',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://github.com/orgs/backstage/teams/team',
|
||||
'github.com/team-slug': 'blah/team',
|
||||
},
|
||||
name: 'team',
|
||||
description: 'The one and only team',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
spec: {
|
||||
type: 'team',
|
||||
profile: {
|
||||
displayName: 'Team',
|
||||
picture: 'http://example.com/team.jpeg',
|
||||
},
|
||||
children: [],
|
||||
parent: 'parent',
|
||||
members: ['a'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'delta',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
OrganizationMemberAddedEvent,
|
||||
OrganizationMemberRemovedEvent,
|
||||
TeamEvent,
|
||||
TeamEditedEvent,
|
||||
MembershipEvent,
|
||||
} from '@octokit/webhooks-types';
|
||||
import { merge } from 'lodash';
|
||||
import * as uuid from 'uuid';
|
||||
@@ -58,7 +60,10 @@ import { TeamTransformer, UserTransformer } from '../lib';
|
||||
import {
|
||||
createAddEntitiesOperation,
|
||||
createRemoveEntitiesOperation,
|
||||
createReplaceEntitiesOperation,
|
||||
DeferredEntitiesBuilder,
|
||||
getOrganizationTeam,
|
||||
getOrganizationTeamsFromUsers,
|
||||
} from '../lib/github';
|
||||
|
||||
/**
|
||||
@@ -255,6 +260,11 @@ export class GithubOrgEntityProvider
|
||||
this.options.gitHubConfig.host,
|
||||
);
|
||||
|
||||
const replaceEntitiesOperation = createReplaceEntitiesOperation(
|
||||
this.options.id,
|
||||
this.options.gitHubConfig.host,
|
||||
);
|
||||
|
||||
// handle change users in the org
|
||||
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#organization
|
||||
if (params.topic.includes('organization')) {
|
||||
@@ -283,14 +293,21 @@ export class GithubOrgEntityProvider
|
||||
: removeEntitiesOperation;
|
||||
await this.onTeamChangeInOrganization(teamEvent, createDeltaOperation);
|
||||
} else if (teamEvent.action === 'edited') {
|
||||
this.onEventToRebuildOrg();
|
||||
await this.onTeamEditedInOrganization(
|
||||
teamEvent,
|
||||
replaceEntitiesOperation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// handle change membership in the org
|
||||
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#membership
|
||||
if (params.topic.includes('membership')) {
|
||||
this.onEventToRebuildOrg();
|
||||
const membershipEvent = params.eventPayload as MembershipEvent;
|
||||
this.onMembershipChangedInOrganization(
|
||||
membershipEvent,
|
||||
replaceEntitiesOperation,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -301,6 +318,143 @@ export class GithubOrgEntityProvider
|
||||
return ['github.organization', 'github.team', 'github.membership'];
|
||||
}
|
||||
|
||||
private async onTeamEditedInOrganization(
|
||||
event: TeamEditedEvent,
|
||||
createDeltaOperation: DeferredEntitiesBuilder,
|
||||
) {
|
||||
if (!this.connection) {
|
||||
throw new Error('Not initialized');
|
||||
}
|
||||
|
||||
const teamSlug = event.team.slug;
|
||||
const { headers, type: tokenType } =
|
||||
await this.credentialsProvider.getCredentials({
|
||||
url: this.options.orgUrl,
|
||||
});
|
||||
const client = graphql.defaults({
|
||||
baseUrl: this.options.gitHubConfig.apiBaseUrl,
|
||||
headers,
|
||||
});
|
||||
|
||||
const { org } = parseGithubOrgUrl(this.options.orgUrl);
|
||||
const { group } = await getOrganizationTeam(
|
||||
client,
|
||||
org,
|
||||
teamSlug,
|
||||
this.options.teamTransformer,
|
||||
);
|
||||
|
||||
const { users } = await getOrganizationUsers(
|
||||
client,
|
||||
org,
|
||||
tokenType,
|
||||
this.options.userTransformer,
|
||||
);
|
||||
|
||||
const usersFromChangedGroup = group.spec.members || [];
|
||||
const usersToRebuild = users.filter(u =>
|
||||
usersFromChangedGroup.includes(u.metadata.name),
|
||||
);
|
||||
|
||||
const { groups } = await getOrganizationTeamsFromUsers(
|
||||
client,
|
||||
org,
|
||||
usersToRebuild.map(u => u.metadata.name),
|
||||
this.options.teamTransformer,
|
||||
);
|
||||
|
||||
assignGroupsToUsers(usersToRebuild, groups);
|
||||
buildOrgHierarchy(groups);
|
||||
|
||||
const oldName = event.changes.name?.from || '';
|
||||
const oldSlug = oldName.toLowerCase().replaceAll(/\s/gi, '-');
|
||||
|
||||
const { removed } = createDeltaOperation(org, [
|
||||
{
|
||||
...group,
|
||||
metadata: {
|
||||
name: oldSlug,
|
||||
},
|
||||
},
|
||||
]);
|
||||
const { added } = createDeltaOperation(org, [...usersToRebuild, ...groups]);
|
||||
await this.connection.applyMutation({
|
||||
type: 'delta',
|
||||
removed,
|
||||
added,
|
||||
});
|
||||
}
|
||||
|
||||
private async onMembershipChangedInOrganization(
|
||||
event: MembershipEvent,
|
||||
createDeltaOperation: DeferredEntitiesBuilder,
|
||||
) {
|
||||
if (!this.connection) {
|
||||
throw new Error('Not initialized');
|
||||
}
|
||||
|
||||
// The docs are saying I will receive the slug for the removed event,
|
||||
// but the types don't reflect that,
|
||||
// so I will just check to be sure the slug is there
|
||||
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#membership
|
||||
if (!('slug' in event.team)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const teamSlug = event.team.slug;
|
||||
const userLogin = event.member.login;
|
||||
const { headers, type: tokenType } =
|
||||
await this.credentialsProvider.getCredentials({
|
||||
url: this.options.orgUrl,
|
||||
});
|
||||
const client = graphql.defaults({
|
||||
baseUrl: this.options.gitHubConfig.apiBaseUrl,
|
||||
headers,
|
||||
});
|
||||
|
||||
const { org } = parseGithubOrgUrl(this.options.orgUrl);
|
||||
const { group } = await getOrganizationTeam(
|
||||
client,
|
||||
org,
|
||||
teamSlug,
|
||||
this.options.teamTransformer,
|
||||
);
|
||||
|
||||
const { users } = await getOrganizationUsers(
|
||||
client,
|
||||
org,
|
||||
tokenType,
|
||||
this.options.userTransformer,
|
||||
);
|
||||
|
||||
const usersToRebuild = users.filter(u => u.metadata.name === userLogin);
|
||||
|
||||
const { groups } = await getOrganizationTeamsFromUsers(
|
||||
client,
|
||||
org,
|
||||
[userLogin],
|
||||
this.options.teamTransformer,
|
||||
);
|
||||
|
||||
// we include group because the removed event need to update the old group too
|
||||
if (!groups.some(g => g.metadata.name === group.metadata.name)) {
|
||||
groups.push(group);
|
||||
}
|
||||
|
||||
assignGroupsToUsers(usersToRebuild, groups);
|
||||
buildOrgHierarchy(groups);
|
||||
|
||||
const { added, removed } = createDeltaOperation(org, [
|
||||
...usersToRebuild,
|
||||
...groups,
|
||||
]);
|
||||
await this.connection.applyMutation({
|
||||
type: 'delta',
|
||||
removed,
|
||||
added,
|
||||
});
|
||||
}
|
||||
|
||||
private async onTeamChangeInOrganization(
|
||||
event: TeamEvent,
|
||||
createDeltaOperation: DeferredEntitiesBuilder,
|
||||
@@ -311,13 +465,7 @@ export class GithubOrgEntityProvider
|
||||
|
||||
const organizationTeamTransformer =
|
||||
this.options.teamTransformer || defaultOrganizationTeamTransformer;
|
||||
const {
|
||||
id: databaseId,
|
||||
name,
|
||||
html_url: url,
|
||||
description,
|
||||
slug,
|
||||
} = event.team;
|
||||
const { name, html_url: url, description, slug } = event.team;
|
||||
const org = event.organization.login;
|
||||
const { headers } = await this.credentialsProvider.getCredentials({
|
||||
url: this.options.orgUrl,
|
||||
@@ -329,7 +477,6 @@ export class GithubOrgEntityProvider
|
||||
|
||||
const group = (await organizationTeamTransformer(
|
||||
{
|
||||
databaseId,
|
||||
name,
|
||||
slug,
|
||||
editTeamUrl: `${url}/edit`,
|
||||
@@ -355,16 +502,6 @@ export class GithubOrgEntityProvider
|
||||
});
|
||||
}
|
||||
|
||||
private onEventToRebuildOrg() {
|
||||
try {
|
||||
this.read(this.options).then(() => {
|
||||
this.options.logger.debug('Event process finished');
|
||||
});
|
||||
} catch (e) {
|
||||
this.options.logger.debug(`Event process error ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async onMemberChangeInOrganization(
|
||||
event: OrganizationMemberAddedEvent | OrganizationMemberRemovedEvent,
|
||||
createDeltaOperation: DeferredEntitiesBuilder,
|
||||
|
||||
Reference in New Issue
Block a user