Skip to content

Commit fb1b2fb

Browse files
refactor: update GitHub issues API to support fetching issues by repository host
1 parent cf73bb4 commit fb1b2fb

3 files changed

Lines changed: 107 additions & 45 deletions

File tree

workspaces/github/plugins/github-issues/src/api/githubIssuesApi.test.ts

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -131,43 +131,89 @@ describe('githubIssuesApi', () => {
131131
entityRepository('mrwolny/yo.yo'),
132132
],
133133
10,
134-
'github.com',
135134
);
136135

137136
expect(mockGraphQLQuery).toHaveBeenCalledTimes(1);
138137
expect(mockGraphQLQuery).toHaveBeenCalledWith(getFragment());
139138
});
140139

141-
it('should only fetch data for entities hosted in the same GitHub instance as is entity location', async () => {
140+
it('should fetch data for repositories grouped by GitHub host', async () => {
142141
await api.fetchIssuesByRepoFromGithub(
143142
[
144143
entityRepository('mrwolny/yo-yo'),
145144
entityRepository('mrwolny/yoyo'),
146145
entityRepository('mrwolny/yo.yo'),
147-
entityRepository('mrwolny/another-repo', 'enterprise.github.com'), // This one should be filtered out
146+
entityRepository('mrwolny/another-repo', 'enterprise.github.com'),
148147
],
149148
10,
150-
'github.com',
151149
);
152150

153-
expect(mockGraphQLQuery).toHaveBeenCalledTimes(1);
154-
expect(mockGraphQLQuery).toHaveBeenCalledWith(getFragment());
151+
expect(mockGraphQLQuery).toHaveBeenCalledTimes(2);
152+
expect(mockGraphQLQuery).toHaveBeenNthCalledWith(1, getFragment());
153+
expect(mockGraphQLQuery).toHaveBeenNthCalledWith(
154+
2,
155+
expect.stringContaining(
156+
'anotherrepo: repository(name: "another-repo", owner: "mrwolny")',
157+
),
158+
);
159+
});
160+
161+
it('should merge issues from repositories across different GitHub hosts', async () => {
162+
mockGraphQLQuery
163+
.mockResolvedValueOnce({
164+
yoyo: {
165+
issues: {
166+
totalCount: 1,
167+
edges: [],
168+
},
169+
},
170+
})
171+
.mockResolvedValueOnce({
172+
anotherrepo: {
173+
issues: {
174+
totalCount: 2,
175+
edges: [],
176+
},
177+
},
178+
});
179+
180+
const data = await api.fetchIssuesByRepoFromGithub(
181+
[
182+
entityRepository('mrwolny/yo-yo'),
183+
entityRepository('mrwolny/another-repo', 'enterprise.github.com'),
184+
],
185+
10,
186+
);
187+
188+
expect(data).toEqual({
189+
'mrwolny/yo-yo': {
190+
issues: {
191+
totalCount: 1,
192+
edges: [],
193+
},
194+
},
195+
'mrwolny/another-repo': {
196+
issues: {
197+
totalCount: 2,
198+
edges: [],
199+
},
200+
},
201+
});
155202
});
156203

157-
it("should only fetch data for entities hosted in the same GitHub instance as the plugin's first config if no config matches", async () => {
204+
it('should ignore repositories without hostname', async () => {
158205
await api.fetchIssuesByRepoFromGithub(
159206
[
160207
entityRepository('mrwolny/yo-yo'),
161-
entityRepository('mrwolny/yoyo'),
162-
entityRepository('mrwolny/yo.yo'),
163-
entityRepository('mrwolny/another-repo', 'enterprise.github.com'), // This one should be filtered out
208+
{
209+
name: 'mrwolny/no-hostname',
210+
locationHostname: '',
211+
},
164212
],
165213
10,
166-
'enterprise.github.com',
167214
);
168215

169216
expect(mockGraphQLQuery).toHaveBeenCalledTimes(1);
170-
expect(mockGraphQLQuery).toHaveBeenCalledWith(getFragment());
171217
});
172218

173219
it('should call Github API with the correct filterBy and orderBy clauses', async () => {
@@ -178,7 +224,6 @@ describe('githubIssuesApi', () => {
178224
entityRepository('mrwolny/yo.yo'),
179225
],
180226
10,
181-
'github.com',
182227
{
183228
filterBy: {
184229
labels: ['bug'],
@@ -290,7 +335,6 @@ describe('githubIssuesApi', () => {
290335
const data = await api.fetchIssuesByRepoFromGithub(
291336
[entityRepository('mrwolny/yo-yo'), entityRepository('mrwolny/notfound')],
292337
10,
293-
'github.com',
294338
);
295339

296340
expect(data).toEqual({
@@ -361,7 +405,6 @@ describe('githubIssuesApi', () => {
361405
const data = await api.fetchIssuesByRepoFromGithub(
362406
[entityRepository('mrwolny/notfound')],
363407
10,
364-
'github.com',
365408
);
366409

367410
expect(data).toEqual({});
@@ -403,7 +446,6 @@ describe('githubIssuesApi', () => {
403446
await api.fetchIssuesByRepoFromGithub(
404447
[entityRepository('mrwolny/notfound')],
405448
10,
406-
'github.com',
407449
);
408450

409451
expect(mockErrorApi.post).toHaveBeenCalledTimes(1);

workspaces/github/plugins/github-issues/src/api/githubIssuesApi.ts

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,13 @@ export const githubIssuesApi = (
133133
});
134134

135135
const baseUrl = githubIntegrationConfig?.apiBaseUrl;
136-
return { octokit: new Octokit({ auth: token, baseUrl }), host };
136+
return { octokit: new Octokit({ auth: token, baseUrl }) };
137137
};
138138

139-
const fetchIssuesByRepoFromGithub = async (
139+
const fetchIssuesByRepoFromHost = async (
140+
hostname: string,
140141
repos: Array<Repository>,
141142
itemsPerRepo: number,
142-
hostname: string,
143143
{
144144
filterBy,
145145
orderBy = {
@@ -148,35 +148,29 @@ export const githubIssuesApi = (
148148
},
149149
}: GithubIssuesByRepoOptions = {},
150150
): Promise<IssuesByRepo> => {
151-
const { octokit, host } = await getOctokit(hostname);
151+
const { octokit } = await getOctokit(hostname);
152152
const safeNames: Array<string> = [];
153-
const repositories = repos
154-
// only tries to fetch issues from repositories that are hosted on the same GitHub instance as the octokit
155-
.filter(repo => repo.locationHostname === host)
156-
.map(repo => {
157-
const [owner, name] = repo.name.split('/');
153+
const repositories = repos.map(repo => {
154+
const [owner, name] = repo.name.split('/');
158155

159-
const safeNameRegex = /-|\./gi;
160-
let safeName = name.replace(safeNameRegex, '');
156+
const safeNameRegex = /-|\./gi;
157+
let safeName = name.replace(safeNameRegex, '');
161158

162-
while (safeNames.includes(safeName)) {
163-
safeName += 'x';
164-
}
159+
while (safeNames.includes(safeName)) {
160+
safeName += 'x';
161+
}
165162

166-
safeNames.push(safeName);
163+
safeNames.push(safeName);
167164

168-
return {
169-
safeName,
170-
name,
171-
owner,
172-
};
173-
});
165+
return {
166+
safeName,
167+
name,
168+
owner,
169+
};
170+
});
174171

175172
let issuesByRepo: IssuesByRepo = {};
176173
try {
177-
if (repositories.length === 0) {
178-
throw new Error(`No repositories found for ${host}`);
179-
}
180174
issuesByRepo = await octokit.graphql(
181175
createIssueByRepoQuery(repositories, itemsPerRepo, {
182176
filterBy,
@@ -200,6 +194,37 @@ export const githubIssuesApi = (
200194
}, {} as IssuesByRepo);
201195
};
202196

197+
const fetchIssuesByRepoFromGithub = async (
198+
repos: Array<Repository>,
199+
itemsPerRepo: number,
200+
options: GithubIssuesByRepoOptions = {},
201+
): Promise<IssuesByRepo> => {
202+
const reposByHost = repos.reduce<Record<string, Array<Repository>>>(
203+
(acc, repo) => {
204+
if (!repo.locationHostname) {
205+
return acc;
206+
}
207+
208+
acc[repo.locationHostname] = acc[repo.locationHostname] ?? [];
209+
acc[repo.locationHostname].push(repo);
210+
211+
return acc;
212+
},
213+
{},
214+
);
215+
216+
const issuesByRepoByHost = await Promise.all(
217+
Object.entries(reposByHost).map(([hostname, hostRepos]) =>
218+
fetchIssuesByRepoFromHost(hostname, hostRepos, itemsPerRepo, options),
219+
),
220+
);
221+
222+
return issuesByRepoByHost.reduce(
223+
(acc, issuesByRepo) => ({ ...acc, ...issuesByRepo }),
224+
{},
225+
);
226+
};
227+
203228
return { fetchIssuesByRepoFromGithub };
204229
};
205230

workspaces/github/plugins/github-issues/src/hooks/useGetIssuesByRepoFromGithub.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,13 @@ import {
2121
githubIssuesApiRef,
2222
GithubIssuesByRepoOptions,
2323
} from '../api';
24-
import { useEntity } from '@backstage/plugin-catalog-react';
25-
import { getHostnameFromEntity } from './useEntityGithubRepositories';
2624

2725
export const useGetIssuesByRepoFromGithub = (
2826
repos: Array<Repository>,
2927
itemsPerRepo: number,
3028
options?: GithubIssuesByRepoOptions,
3129
) => {
3230
const githubIssuesApi = useApi(githubIssuesApiRef);
33-
const { entity } = useEntity();
34-
const hostname = getHostnameFromEntity(entity);
3531

3632
const {
3733
value: issues,
@@ -42,7 +38,6 @@ export const useGetIssuesByRepoFromGithub = (
4238
return await githubIssuesApi.fetchIssuesByRepoFromGithub(
4339
repos,
4440
itemsPerRepo,
45-
hostname,
4641
options,
4742
);
4843
}

0 commit comments

Comments
 (0)