Merge pull request #19311 from jgrigg/relax-github-transforms
Relax github transform types
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-github': patch
|
||||
---
|
||||
|
||||
Allow github user and team transforms to return any Entity
|
||||
@@ -16,7 +16,6 @@ import { EventSubscriber } from '@backstage/plugin-events-node';
|
||||
import { GithubCredentialsProvider } from '@backstage/integration';
|
||||
import { GithubIntegrationConfig } from '@backstage/integration';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
import { GroupEntity } from '@backstage/catalog-model';
|
||||
import { LocationSpec } from '@backstage/plugin-catalog-node';
|
||||
import { Logger } from 'winston';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
@@ -25,7 +24,6 @@ import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { ScmLocationAnalyzer } from '@backstage/plugin-catalog-backend';
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
import { TokenManager } from '@backstage/backend-common';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
|
||||
// @public
|
||||
export const defaultOrganizationTeamTransformer: TeamTransformer;
|
||||
@@ -303,7 +301,7 @@ export type GithubUser = {
|
||||
export type TeamTransformer = (
|
||||
item: GithubTeam,
|
||||
ctx: TransformerContext,
|
||||
) => Promise<GroupEntity | undefined>;
|
||||
) => Promise<Entity | undefined>;
|
||||
|
||||
// @public
|
||||
export interface TransformerContext {
|
||||
@@ -319,5 +317,5 @@ export interface TransformerContext {
|
||||
export type UserTransformer = (
|
||||
item: GithubUser,
|
||||
ctx: TransformerContext,
|
||||
) => Promise<UserEntity | undefined>;
|
||||
) => Promise<Entity | undefined>;
|
||||
```
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
|
||||
import { Entity, GroupEntity, UserEntity } from '@backstage/catalog-model';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
import {
|
||||
ANNOTATION_GITHUB_TEAM_SLUG,
|
||||
@@ -34,24 +34,24 @@ export interface TransformerContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformer for GitHub users to UserEntity
|
||||
* Transformer for GitHub users to an Entity
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type UserTransformer = (
|
||||
item: GithubUser,
|
||||
ctx: TransformerContext,
|
||||
) => Promise<UserEntity | undefined>;
|
||||
) => Promise<Entity | undefined>;
|
||||
|
||||
/**
|
||||
* Transformer for GitHub Team to GroupEntity
|
||||
* Transformer for GitHub Team to an Entity
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type TeamTransformer = (
|
||||
item: GithubTeam,
|
||||
ctx: TransformerContext,
|
||||
) => Promise<GroupEntity | undefined>;
|
||||
) => Promise<Entity | undefined>;
|
||||
|
||||
/**
|
||||
* Default transformer for GitHub users to UserEntity
|
||||
|
||||
@@ -229,7 +229,7 @@ describe('github', () => {
|
||||
|
||||
it('reads teams', async () => {
|
||||
const output = {
|
||||
groups: [
|
||||
teams: [
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
name: 'team',
|
||||
@@ -329,7 +329,7 @@ describe('github', () => {
|
||||
|
||||
it('reads teams', async () => {
|
||||
const output = {
|
||||
groups: [
|
||||
teams: [
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
name: 'Team-custom',
|
||||
@@ -409,7 +409,7 @@ describe('github', () => {
|
||||
};
|
||||
|
||||
const output = {
|
||||
groups: [
|
||||
teams: [
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
name: 'Team-custom',
|
||||
@@ -444,7 +444,7 @@ describe('github', () => {
|
||||
customTeamTransformer,
|
||||
);
|
||||
|
||||
expect(teams.groups).toHaveLength(1);
|
||||
expect(teams.teams).toHaveLength(1);
|
||||
expect(teams).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, GroupEntity, UserEntity } from '@backstage/catalog-model';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { GithubCredentialType } from '@backstage/integration';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
import {
|
||||
@@ -139,7 +139,7 @@ export async function getOrganizationUsers(
|
||||
org: string,
|
||||
tokenType: GithubCredentialType,
|
||||
userTransformer: UserTransformer = defaultUserTransformer,
|
||||
): Promise<{ users: UserEntity[] }> {
|
||||
): Promise<{ users: Entity[] }> {
|
||||
const query = `
|
||||
query users($org: String!, $email: Boolean!, $cursor: String) {
|
||||
organization(login: $org) {
|
||||
@@ -188,7 +188,7 @@ export async function getOrganizationTeams(
|
||||
org: string,
|
||||
teamTransformer: TeamTransformer = defaultOrganizationTeamTransformer,
|
||||
): Promise<{
|
||||
groups: GroupEntity[];
|
||||
teams: Entity[];
|
||||
}> {
|
||||
const query = `
|
||||
query teams($org: String!, $cursor: String) {
|
||||
@@ -222,7 +222,7 @@ export async function getOrganizationTeams(
|
||||
const materialisedTeams = async (
|
||||
item: GithubTeamResponse,
|
||||
ctx: TransformerContext,
|
||||
): Promise<GroupEntity | undefined> => {
|
||||
): Promise<Entity | undefined> => {
|
||||
const memberNames: GithubUser[] = [];
|
||||
|
||||
if (!item.members.pageInfo.hasNextPage) {
|
||||
@@ -247,7 +247,7 @@ export async function getOrganizationTeams(
|
||||
return await teamTransformer(team, ctx);
|
||||
};
|
||||
|
||||
const groups = await queryWithPaging(
|
||||
const teams = await queryWithPaging(
|
||||
client,
|
||||
query,
|
||||
org,
|
||||
@@ -256,7 +256,7 @@ export async function getOrganizationTeams(
|
||||
{ org },
|
||||
);
|
||||
|
||||
return { groups };
|
||||
return { teams };
|
||||
}
|
||||
|
||||
export async function getOrganizationTeamsFromUsers(
|
||||
@@ -265,7 +265,7 @@ export async function getOrganizationTeamsFromUsers(
|
||||
userLogins: string[],
|
||||
teamTransformer: TeamTransformer = defaultOrganizationTeamTransformer,
|
||||
): Promise<{
|
||||
groups: GroupEntity[];
|
||||
teams: Entity[];
|
||||
}> {
|
||||
const query = `
|
||||
query teams($org: String!, $cursor: String, $userLogins: [String!] = "") {
|
||||
@@ -306,7 +306,7 @@ export async function getOrganizationTeamsFromUsers(
|
||||
const materialisedTeams = async (
|
||||
item: GithubTeamResponse,
|
||||
ctx: TransformerContext,
|
||||
): Promise<GroupEntity | undefined> => {
|
||||
): Promise<Entity | undefined> => {
|
||||
const memberNames: GithubUser[] = [];
|
||||
|
||||
if (!item.members.pageInfo.hasNextPage) {
|
||||
@@ -331,7 +331,7 @@ export async function getOrganizationTeamsFromUsers(
|
||||
return await teamTransformer(team, ctx);
|
||||
};
|
||||
|
||||
const groups = await queryWithPaging(
|
||||
const teams = await queryWithPaging(
|
||||
client,
|
||||
query,
|
||||
org,
|
||||
@@ -340,7 +340,7 @@ export async function getOrganizationTeamsFromUsers(
|
||||
{ org, userLogins },
|
||||
);
|
||||
|
||||
return { groups };
|
||||
return { teams };
|
||||
}
|
||||
|
||||
export async function getOrganizationsFromUser(
|
||||
@@ -377,7 +377,7 @@ export async function getOrganizationTeam(
|
||||
teamSlug: string,
|
||||
teamTransformer: TeamTransformer = defaultOrganizationTeamTransformer,
|
||||
): Promise<{
|
||||
group: GroupEntity;
|
||||
team: Entity;
|
||||
}> {
|
||||
const query = `
|
||||
query teams($org: String!, $teamSlug: String!) {
|
||||
@@ -401,7 +401,7 @@ export async function getOrganizationTeam(
|
||||
const materialisedTeam = async (
|
||||
item: GithubTeamResponse,
|
||||
ctx: TransformerContext,
|
||||
): Promise<GroupEntity | undefined> => {
|
||||
): Promise<Entity | undefined> => {
|
||||
const memberNames: GithubUser[] = [];
|
||||
|
||||
if (!item.members.pageInfo.hasNextPage) {
|
||||
@@ -432,17 +432,17 @@ export async function getOrganizationTeam(
|
||||
});
|
||||
|
||||
if (!response.organization?.team)
|
||||
throw new Error(`Found no match for group ${teamSlug}`);
|
||||
throw new Error(`Found no match for team ${teamSlug}`);
|
||||
|
||||
const group = await materialisedTeam(response.organization?.team, {
|
||||
const team = await materialisedTeam(response.organization?.team, {
|
||||
query,
|
||||
client,
|
||||
org,
|
||||
});
|
||||
|
||||
if (!group) throw new Error(`Can't transform for group ${teamSlug}`);
|
||||
if (!team) throw new Error(`Can't transform for team ${teamSlug}`);
|
||||
|
||||
return { group };
|
||||
return { team };
|
||||
}
|
||||
|
||||
export async function getOrganizationRepositories(
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2023 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 {
|
||||
Entity,
|
||||
GroupEntity,
|
||||
UserEntity,
|
||||
isGroupEntity,
|
||||
isUserEntity,
|
||||
} from '@backstage/catalog-model';
|
||||
|
||||
export function areGroupEntities(
|
||||
entities: Entity[],
|
||||
): entities is GroupEntity[] {
|
||||
return entities.every(e => isGroupEntity(e));
|
||||
}
|
||||
|
||||
export function areUserEntities(entities: Entity[]): entities is UserEntity[] {
|
||||
return entities.every(e => isUserEntity(e));
|
||||
}
|
||||
+16
-11
@@ -16,9 +16,9 @@
|
||||
|
||||
import {
|
||||
DEFAULT_NAMESPACE,
|
||||
GroupEntity,
|
||||
Entity,
|
||||
isGroupEntity,
|
||||
stringifyEntityRef,
|
||||
UserEntity,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
TeamTransformer,
|
||||
UserTransformer,
|
||||
} from '../lib';
|
||||
import { areGroupEntities, areUserEntities } from '../lib/guards';
|
||||
|
||||
/**
|
||||
* Extracts teams and users out of a multiple GitHub orgs namespaced per org.
|
||||
@@ -147,7 +148,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor {
|
||||
client,
|
||||
orgConfig.name,
|
||||
tokenType,
|
||||
async (githubUser, ctx): Promise<UserEntity | undefined> => {
|
||||
async (githubUser, ctx): Promise<Entity | undefined> => {
|
||||
const result = this.options.userTransformer
|
||||
? await this.options.userTransformer(githubUser, ctx)
|
||||
: await defaultUserTransformer(githubUser, ctx);
|
||||
@@ -160,15 +161,15 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor {
|
||||
},
|
||||
);
|
||||
|
||||
const { groups } = await getOrganizationTeams(
|
||||
const { teams } = await getOrganizationTeams(
|
||||
client,
|
||||
orgConfig.name,
|
||||
async (team, ctx): Promise<GroupEntity | undefined> => {
|
||||
async (team, ctx): Promise<Entity | undefined> => {
|
||||
const result = this.options.teamTransformer
|
||||
? await this.options.teamTransformer(team, ctx)
|
||||
: await defaultOrganizationTeamTransformer(team, ctx);
|
||||
|
||||
if (result) {
|
||||
if (result && isGroupEntity(result)) {
|
||||
result.metadata.namespace = orgConfig.groupNamespace;
|
||||
// Group `spec.members` inherits the namespace of it's group so need to explicitly specify refs here
|
||||
result.spec.members = team.members.map(
|
||||
@@ -185,7 +186,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor {
|
||||
|
||||
const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1);
|
||||
this.logger.debug(
|
||||
`Read ${users.length} GitHub users and ${groups.length} GitHub teams from ${orgConfig.name} in ${duration} seconds`,
|
||||
`Read ${users.length} GitHub users and ${teams.length} GitHub teams from ${orgConfig.name} in ${duration} seconds`,
|
||||
);
|
||||
|
||||
// Grab current users from `allUsersMap` if they already exist in our
|
||||
@@ -199,11 +200,15 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor {
|
||||
return allUsersMap.get(userRef);
|
||||
});
|
||||
|
||||
assignGroupsToUsers(pendingUsers, groups);
|
||||
buildOrgHierarchy(groups);
|
||||
if (areGroupEntities(teams)) {
|
||||
buildOrgHierarchy(teams);
|
||||
if (areUserEntities(pendingUsers)) {
|
||||
assignGroupsToUsers(pendingUsers, teams);
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
emit(processingResult.entity(location, group));
|
||||
for (const team of teams) {
|
||||
emit(processingResult.entity(location, team));
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
getOrganizationUsers,
|
||||
parseGithubOrgUrl,
|
||||
} from '../lib';
|
||||
import { areGroupEntities, areUserEntities } from '../lib/guards';
|
||||
|
||||
type GraphQL = typeof graphql;
|
||||
|
||||
@@ -101,19 +102,23 @@ export class GithubOrgReaderProcessor implements CatalogProcessor {
|
||||
this.logger.info('Reading GitHub users and groups');
|
||||
|
||||
const { users } = await getOrganizationUsers(client, org, tokenType);
|
||||
const { groups } = await getOrganizationTeams(client, org);
|
||||
const { teams } = await getOrganizationTeams(client, org);
|
||||
|
||||
const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1);
|
||||
this.logger.debug(
|
||||
`Read ${users.length} GitHub users and ${groups.length} GitHub groups in ${duration} seconds`,
|
||||
`Read ${users.length} GitHub users and ${teams.length} GitHub teams in ${duration} seconds`,
|
||||
);
|
||||
|
||||
assignGroupsToUsers(users, groups);
|
||||
buildOrgHierarchy(groups);
|
||||
if (areGroupEntities(teams)) {
|
||||
buildOrgHierarchy(teams);
|
||||
if (areUserEntities(users)) {
|
||||
assignGroupsToUsers(users, teams);
|
||||
}
|
||||
}
|
||||
|
||||
// Done!
|
||||
for (const group of groups) {
|
||||
emit(processingResult.entity(location, group));
|
||||
for (const team of teams) {
|
||||
emit(processingResult.entity(location, team));
|
||||
}
|
||||
for (const user of users) {
|
||||
emit(processingResult.entity(location, user));
|
||||
|
||||
+44
-30
@@ -20,7 +20,7 @@ import {
|
||||
ANNOTATION_ORIGIN_LOCATION,
|
||||
DEFAULT_NAMESPACE,
|
||||
Entity,
|
||||
GroupEntity,
|
||||
isGroupEntity,
|
||||
parseEntityRef,
|
||||
stringifyEntityRef,
|
||||
UserEntity,
|
||||
@@ -78,6 +78,7 @@ import {
|
||||
getOrganizationTeamsFromUsers,
|
||||
} from '../lib/github';
|
||||
import { splitTeamSlug } from '../lib/util';
|
||||
import { areGroupEntities, areUserEntities } from '../lib/guards';
|
||||
|
||||
/**
|
||||
* Options for {@link GithubMultiOrgEntityProvider}.
|
||||
@@ -240,7 +241,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
const { markReadComplete } = trackProgress(logger);
|
||||
|
||||
const allUsersMap = new Map();
|
||||
const allGroups: Entity[] = [];
|
||||
const allTeams: Entity[] = [];
|
||||
|
||||
const orgsToProcess = this.options.orgs?.length
|
||||
? this.options.orgs
|
||||
@@ -266,7 +267,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
this.options.userTransformer,
|
||||
);
|
||||
|
||||
const { groups } = await getOrganizationTeams(
|
||||
const { teams } = await getOrganizationTeams(
|
||||
client,
|
||||
org,
|
||||
this.defaultMultiOrgTeamTransformer.bind(this),
|
||||
@@ -283,10 +284,14 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
return allUsersMap.get(userRef);
|
||||
});
|
||||
|
||||
assignGroupsToUsers(pendingUsers, groups);
|
||||
buildOrgHierarchy(groups);
|
||||
if (areGroupEntities(teams)) {
|
||||
buildOrgHierarchy(teams);
|
||||
if (areUserEntities(pendingUsers)) {
|
||||
assignGroupsToUsers(pendingUsers, teams);
|
||||
}
|
||||
}
|
||||
|
||||
allGroups.push(...groups);
|
||||
allTeams.push(...teams);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to read GitHub org data for ${org}: ${e}`);
|
||||
}
|
||||
@@ -294,11 +299,11 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
|
||||
const allUsers = Array.from(allUsersMap.values());
|
||||
|
||||
const { markCommitComplete } = markReadComplete({ allUsers, allGroups });
|
||||
const { markCommitComplete } = markReadComplete({ allUsers, allTeams });
|
||||
|
||||
await this.connection.applyMutation({
|
||||
type: 'full',
|
||||
entities: [...allUsers, ...allGroups].map(entity => ({
|
||||
entities: [...allUsers, ...allTeams].map(entity => ({
|
||||
locationKey: `github-multi-org-provider:${this.options.id}`,
|
||||
entity: withLocations(
|
||||
`https://${this.options.gitHubConfig.host}`,
|
||||
@@ -422,7 +427,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
this.options.userTransformer,
|
||||
);
|
||||
|
||||
const { groups } = await getOrganizationTeams(
|
||||
const { teams } = await getOrganizationTeams(
|
||||
client,
|
||||
org,
|
||||
this.defaultMultiOrgTeamTransformer.bind(this),
|
||||
@@ -440,7 +445,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
headers: orgHeaders,
|
||||
});
|
||||
|
||||
const { groups: userGroups } = await getOrganizationTeamsFromUsers(
|
||||
const { teams: userTeams } = await getOrganizationTeamsFromUsers(
|
||||
orgClient,
|
||||
userOrg,
|
||||
users.map(
|
||||
@@ -451,12 +456,14 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
this.defaultMultiOrgTeamTransformer.bind(this),
|
||||
);
|
||||
|
||||
assignGroupsToUsers(users, userGroups);
|
||||
if (areGroupEntities(userTeams) && areUserEntities(users)) {
|
||||
assignGroupsToUsers(users, userTeams);
|
||||
}
|
||||
}
|
||||
|
||||
const { added, removed } = this.createAddEntitiesOperation([
|
||||
...users,
|
||||
...groups,
|
||||
...teams,
|
||||
]);
|
||||
await this.connection.applyMutation({
|
||||
type: 'delta',
|
||||
@@ -536,14 +543,16 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
headers: orgHeaders,
|
||||
});
|
||||
|
||||
const { groups } = await getOrganizationTeamsFromUsers(
|
||||
const { teams } = await getOrganizationTeamsFromUsers(
|
||||
orgClient,
|
||||
userOrg,
|
||||
[login],
|
||||
this.defaultMultiOrgTeamTransformer.bind(this),
|
||||
);
|
||||
|
||||
assignGroupsToUsers([user], groups);
|
||||
if (areGroupEntities(teams)) {
|
||||
assignGroupsToUsers([user], teams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,7 +632,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
});
|
||||
|
||||
const teamSlug = event.team.slug;
|
||||
const { group } = await getOrganizationTeam(
|
||||
const { team } = await getOrganizationTeam(
|
||||
client,
|
||||
org,
|
||||
teamSlug,
|
||||
@@ -637,10 +646,11 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
this.options.userTransformer,
|
||||
);
|
||||
|
||||
const usersFromChangedGroup =
|
||||
group.spec.members?.map(m =>
|
||||
stringifyEntityRef(parseEntityRef(m, { defaultKind: 'user' })),
|
||||
) || [];
|
||||
const usersFromChangedGroup = isGroupEntity(team)
|
||||
? team.spec.members?.map(m =>
|
||||
stringifyEntityRef(parseEntityRef(m, { defaultKind: 'user' })),
|
||||
) || []
|
||||
: [];
|
||||
const usersToRebuild = users.filter(u =>
|
||||
usersFromChangedGroup.includes(stringifyEntityRef(u)),
|
||||
);
|
||||
@@ -656,7 +666,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
headers: orgHeaders,
|
||||
});
|
||||
|
||||
const { groups } = await getOrganizationTeamsFromUsers(
|
||||
const { teams } = await getOrganizationTeamsFromUsers(
|
||||
orgClient,
|
||||
userOrg,
|
||||
usersToRebuild.map(
|
||||
@@ -667,7 +677,9 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
this.defaultMultiOrgTeamTransformer.bind(this),
|
||||
);
|
||||
|
||||
assignGroupsToUsers(usersToRebuild, groups);
|
||||
if (areGroupEntities(teams) && areUserEntities(usersToRebuild)) {
|
||||
assignGroupsToUsers(usersToRebuild, teams);
|
||||
}
|
||||
}
|
||||
|
||||
const oldName = event.changes.name?.from || '';
|
||||
@@ -693,7 +705,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
const { removed } = this.createRemoveEntitiesOperation([oldGroup]);
|
||||
const { added } = this.createAddEntitiesOperation([
|
||||
...usersToRebuild,
|
||||
group,
|
||||
team,
|
||||
]);
|
||||
await this.connection.applyMutation({
|
||||
type: 'delta',
|
||||
@@ -729,7 +741,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
});
|
||||
|
||||
const teamSlug = event.team.slug;
|
||||
const { group } = await getOrganizationTeam(
|
||||
const { team } = await getOrganizationTeam(
|
||||
client,
|
||||
org,
|
||||
teamSlug,
|
||||
@@ -765,17 +777,19 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
headers: orgHeaders,
|
||||
});
|
||||
|
||||
const { groups } = await getOrganizationTeamsFromUsers(
|
||||
const { teams } = await getOrganizationTeamsFromUsers(
|
||||
orgClient,
|
||||
userOrg,
|
||||
[login],
|
||||
this.defaultMultiOrgTeamTransformer.bind(this),
|
||||
);
|
||||
|
||||
assignGroupsToUsers([user], groups);
|
||||
if (areGroupEntities(teams)) {
|
||||
assignGroupsToUsers([user], teams);
|
||||
}
|
||||
}
|
||||
|
||||
const { added, removed } = this.createAddEntitiesOperation([user, group]);
|
||||
const { added, removed } = this.createAddEntitiesOperation([user, team]);
|
||||
await this.connection.applyMutation({
|
||||
type: 'delta',
|
||||
removed,
|
||||
@@ -812,14 +826,14 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
|
||||
private async defaultMultiOrgTeamTransformer(
|
||||
team: GithubTeam,
|
||||
ctx: TransformerContext,
|
||||
): Promise<GroupEntity | undefined> {
|
||||
): Promise<Entity | undefined> {
|
||||
if (this.options.teamTransformer) {
|
||||
return await this.options.teamTransformer(team, ctx);
|
||||
}
|
||||
|
||||
const result = await defaultOrganizationTeamTransformer(team, ctx);
|
||||
|
||||
if (result) {
|
||||
if (result && result.spec) {
|
||||
result.metadata.namespace = ctx.org.toLocaleLowerCase('en-US');
|
||||
// Group `spec.members` inherits the namespace of it's group so need to explicitly specify refs here
|
||||
result.spec.members = team.members.map(
|
||||
@@ -885,9 +899,9 @@ function trackProgress(logger: Logger) {
|
||||
|
||||
function markReadComplete(read: {
|
||||
allUsers: unknown[];
|
||||
allGroups: unknown[];
|
||||
allTeams: unknown[];
|
||||
}) {
|
||||
summary = `${read.allUsers.length} GitHub users and ${read.allGroups.length} GitHub groups`;
|
||||
summary = `${read.allUsers.length} GitHub users and ${read.allTeams.length} GitHub groups`;
|
||||
const readDuration = ((Date.now() - timestamp) / 1000).toFixed(1);
|
||||
timestamp = Date.now();
|
||||
logger.info(`Read ${summary} in ${readDuration} seconds. Committing...`);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
ANNOTATION_LOCATION,
|
||||
ANNOTATION_ORIGIN_LOCATION,
|
||||
Entity,
|
||||
isGroupEntity,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
@@ -68,6 +69,7 @@ import {
|
||||
} from '../lib/github';
|
||||
import { assignGroupsToUsers, buildOrgHierarchy } from '../lib/org';
|
||||
import { parseGithubOrgUrl, splitTeamSlug } from '../lib/util';
|
||||
import { areGroupEntities, areUserEntities } from '../lib/guards';
|
||||
|
||||
/**
|
||||
* Options for {@link GithubOrgEntityProvider}.
|
||||
@@ -223,20 +225,24 @@ export class GithubOrgEntityProvider
|
||||
tokenType,
|
||||
this.options.userTransformer,
|
||||
);
|
||||
const { groups } = await getOrganizationTeams(
|
||||
const { teams } = await getOrganizationTeams(
|
||||
client,
|
||||
org,
|
||||
this.options.teamTransformer,
|
||||
);
|
||||
|
||||
assignGroupsToUsers(users, groups);
|
||||
buildOrgHierarchy(groups);
|
||||
if (areGroupEntities(teams)) {
|
||||
buildOrgHierarchy(teams);
|
||||
if (areUserEntities(users)) {
|
||||
assignGroupsToUsers(users, teams);
|
||||
}
|
||||
}
|
||||
|
||||
const { markCommitComplete } = markReadComplete({ users, groups });
|
||||
const { markCommitComplete } = markReadComplete({ users, teams });
|
||||
|
||||
await this.connection.applyMutation({
|
||||
type: 'full',
|
||||
entities: [...users, ...groups].map(entity => ({
|
||||
entities: [...users, ...teams].map(entity => ({
|
||||
locationKey: `github-org-provider:${this.options.id}`,
|
||||
entity: withLocations(
|
||||
`https://${this.options.gitHubConfig.host}`,
|
||||
@@ -340,7 +346,7 @@ export class GithubOrgEntityProvider
|
||||
});
|
||||
|
||||
const { org } = parseGithubOrgUrl(this.options.orgUrl);
|
||||
const { group } = await getOrganizationTeam(
|
||||
const { team } = await getOrganizationTeam(
|
||||
client,
|
||||
org,
|
||||
teamSlug,
|
||||
@@ -354,20 +360,28 @@ export class GithubOrgEntityProvider
|
||||
this.options.userTransformer,
|
||||
);
|
||||
|
||||
const usersFromChangedGroup = group.spec.members || [];
|
||||
if (!isGroupEntity(team)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const usersFromChangedGroup = team.spec.members || [];
|
||||
const usersToRebuild = users.filter(u =>
|
||||
usersFromChangedGroup.includes(u.metadata.name),
|
||||
);
|
||||
|
||||
const { groups } = await getOrganizationTeamsFromUsers(
|
||||
const { teams } = await getOrganizationTeamsFromUsers(
|
||||
client,
|
||||
org,
|
||||
usersToRebuild.map(u => u.metadata.name),
|
||||
this.options.teamTransformer,
|
||||
);
|
||||
|
||||
assignGroupsToUsers(usersToRebuild, groups);
|
||||
buildOrgHierarchy(groups);
|
||||
if (areGroupEntities(teams)) {
|
||||
buildOrgHierarchy(teams);
|
||||
if (areUserEntities(usersToRebuild)) {
|
||||
assignGroupsToUsers(usersToRebuild, teams);
|
||||
}
|
||||
}
|
||||
|
||||
const oldName = event.changes.name?.from || event.team.name;
|
||||
const oldSlug = oldName.toLowerCase().replaceAll(/\s/gi, '-');
|
||||
@@ -380,14 +394,14 @@ export class GithubOrgEntityProvider
|
||||
|
||||
const { removed } = createDeltaOperation(org, [
|
||||
{
|
||||
...group,
|
||||
...team,
|
||||
metadata: {
|
||||
name: oldSlug,
|
||||
description: oldDescriptionSlug,
|
||||
},
|
||||
},
|
||||
]);
|
||||
const { added } = createDeltaOperation(org, [...usersToRebuild, ...groups]);
|
||||
const { added } = createDeltaOperation(org, [...usersToRebuild, ...teams]);
|
||||
await this.connection.applyMutation({
|
||||
type: 'delta',
|
||||
removed,
|
||||
@@ -423,7 +437,7 @@ export class GithubOrgEntityProvider
|
||||
});
|
||||
|
||||
const { org } = parseGithubOrgUrl(this.options.orgUrl);
|
||||
const { group } = await getOrganizationTeam(
|
||||
const { team } = await getOrganizationTeam(
|
||||
client,
|
||||
org,
|
||||
teamSlug,
|
||||
@@ -439,7 +453,7 @@ export class GithubOrgEntityProvider
|
||||
|
||||
const usersToRebuild = users.filter(u => u.metadata.name === userLogin);
|
||||
|
||||
const { groups } = await getOrganizationTeamsFromUsers(
|
||||
const { teams } = await getOrganizationTeamsFromUsers(
|
||||
client,
|
||||
org,
|
||||
[userLogin],
|
||||
@@ -447,16 +461,20 @@ export class GithubOrgEntityProvider
|
||||
);
|
||||
|
||||
// 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);
|
||||
if (!teams.some(t => t.metadata.name === team.metadata.name)) {
|
||||
teams.push(team);
|
||||
}
|
||||
|
||||
assignGroupsToUsers(usersToRebuild, groups);
|
||||
buildOrgHierarchy(groups);
|
||||
if (areGroupEntities(teams)) {
|
||||
buildOrgHierarchy(teams);
|
||||
if (areUserEntities(usersToRebuild)) {
|
||||
assignGroupsToUsers(usersToRebuild, teams);
|
||||
}
|
||||
}
|
||||
|
||||
const { added, removed } = createDeltaOperation(org, [
|
||||
...usersToRebuild,
|
||||
...groups,
|
||||
...teams,
|
||||
]);
|
||||
await this.connection.applyMutation({
|
||||
type: 'delta',
|
||||
@@ -588,10 +606,10 @@ function trackProgress(logger: Logger) {
|
||||
let timestamp = Date.now();
|
||||
let summary: string;
|
||||
|
||||
logger.info('Reading GitHub users and groups');
|
||||
logger.info('Reading GitHub users and teams');
|
||||
|
||||
function markReadComplete(read: { users: unknown[]; groups: unknown[] }) {
|
||||
summary = `${read.users.length} GitHub users and ${read.groups.length} GitHub groups`;
|
||||
function markReadComplete(read: { users: unknown[]; teams: unknown[] }) {
|
||||
summary = `${read.users.length} GitHub users and ${read.teams.length} GitHub teams`;
|
||||
const readDuration = ((Date.now() - timestamp) / 1000).toFixed(1);
|
||||
timestamp = Date.now();
|
||||
logger.info(`Read ${summary} in ${readDuration} seconds. Committing...`);
|
||||
|
||||
Reference in New Issue
Block a user