Skip to content

Commit d1e739f

Browse files
authored
Fix github team checking for pagination (#669)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved GitHub team detection to reliably handle large team lists during team existence checks. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent e555016 commit d1e739f

4 files changed

Lines changed: 86 additions & 28 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "infra-core",
3-
"version": "4.9.2",
3+
"version": "4.9.3",
44
"private": true,
55
"type": "module",
66
"workspaces": [

src/api/functions/github.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,22 @@ export async function createGithubTeam({
125125
auth,
126126
});
127127
logger.info(`Checking if GitHub team "${name}" exists`);
128-
const teamsResponse = await octokit.request("GET /orgs/{org}/teams", {
129-
org: orgId,
130-
});
128+
let existingTeam: { name: string; id: number } | undefined;
129+
for await (const response of octokit.paginate.iterator(
130+
"GET /orgs/{org}/teams",
131+
{
132+
org: orgId,
133+
per_page: 100,
134+
},
135+
)) {
136+
existingTeam = response.data.find(
137+
(team: { name: string; id: number }) => team.name === name,
138+
);
131139

132-
const existingTeam = teamsResponse.data.find(
133-
(team: { name: string; id: number }) => team.name === name,
134-
);
140+
if (existingTeam) {
141+
break;
142+
}
143+
}
135144

136145
if (existingTeam) {
137146
logger.info(`Team "${name}" already exists with id: ${existingTeam.id}`);

tests/e2e/orgInfo.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ describe("Organization Info Tests", () => {
1616
await expect(page.getByRole("heading")).toContainText(
1717
"Manage Organization Info",
1818
);
19-
await page.getByRole("textbox", { name: "Select an organization" }).click();
19+
await page
20+
.getByRole("combobox", { name: "Select an organization" })
21+
.click();
2022
await page.getByText("Infrastructure Committee").click();
2123
await page.getByRole("textbox", { name: "Description" }).click();
2224
await page

tests/unit/functions/github.test.ts

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ import { RequestError } from "@octokit/request-error";
1212
vi.mock("octokit");
1313
vi.mock("../../../src/api/utils.js");
1414

15+
// Helper to create an async generator from an array of pages
16+
function createAsyncIterator<T>(pages: T[]) {
17+
return (async function* () {
18+
for (const page of pages) {
19+
yield page;
20+
}
21+
})();
22+
}
23+
1524
describe("createGithubTeam", () => {
1625
const mockLogger = {
1726
info: vi.fn(() => {}),
@@ -23,7 +32,7 @@ describe("createGithubTeam", () => {
2332
auth: {
2433
appId: 1,
2534
installationId: 1,
26-
privateKey: "abc"
35+
privateKey: "abc",
2736
},
2837
orgId: "test-org",
2938
parentTeamId: 123,
@@ -39,8 +48,13 @@ describe("createGithubTeam", () => {
3948
vi.clearAllMocks();
4049
mockOctokit = {
4150
request: vi.fn(() => {}),
51+
paginate: {
52+
iterator: vi.fn(),
53+
},
4254
};
43-
(Octokit as any).mockImplementation(function() { return mockOctokit; });
55+
(Octokit as any).mockImplementation(function () {
56+
return mockOctokit;
57+
});
4458
});
4559

4660
afterEach(() => {
@@ -49,26 +63,32 @@ describe("createGithubTeam", () => {
4963

5064
it("should return existing team ID if team already exists", async () => {
5165
const existingTeamId = 456;
52-
mockOctokit.request.mockResolvedValueOnce({
53-
data: [
54-
{ name: "Other Team", id: 789 },
55-
{ name: "Test Team", id: existingTeamId },
56-
],
57-
});
66+
mockOctokit.paginate.iterator.mockReturnValue(
67+
createAsyncIterator([
68+
{
69+
data: [
70+
{ name: "Other Team", id: 789 },
71+
{ name: "Test Team", id: existingTeamId },
72+
],
73+
},
74+
])
75+
);
5876

5977
const result = await createGithubTeam(defaultInputs);
6078

6179
expect(result).toStrictEqual({ updated: false, id: existingTeamId });
6280
expect(mockLogger.info).toHaveBeenCalledWith(
6381
`Team "Test Team" already exists with id: ${existingTeamId}`
6482
);
65-
expect(mockOctokit.request).toHaveBeenCalledTimes(1);
83+
expect(mockOctokit.request).not.toHaveBeenCalled();
6684
});
6785

6886
it("should create new team", async () => {
6987
const newTeamId = 999;
7088
// Mock getting teams (no existing team)
71-
mockOctokit.request.mockResolvedValueOnce({ data: [] });
89+
mockOctokit.paginate.iterator.mockReturnValue(
90+
createAsyncIterator([{ data: [] }])
91+
);
7292

7393
// Mock creating team
7494
mockOctokit.request.mockResolvedValueOnce({
@@ -100,7 +120,9 @@ describe("createGithubTeam", () => {
100120
const inputsWithoutDescription = { ...defaultInputs };
101121
delete (inputsWithoutDescription as any).description;
102122

103-
mockOctokit.request.mockResolvedValueOnce({ data: [] });
123+
mockOctokit.paginate.iterator.mockReturnValue(
124+
createAsyncIterator([{ data: [] }])
125+
);
104126
mockOctokit.request.mockResolvedValueOnce({
105127
status: 201,
106128
data: { id: newTeamId, slug: "test-team" },
@@ -123,7 +145,9 @@ describe("createGithubTeam", () => {
123145
const inputsWithoutPrivacy = { ...defaultInputs };
124146
delete (inputsWithoutPrivacy as any).privacy;
125147

126-
mockOctokit.request.mockResolvedValueOnce({ data: [] });
148+
mockOctokit.paginate.iterator.mockReturnValue(
149+
createAsyncIterator([{ data: [] }])
150+
);
127151
mockOctokit.request.mockResolvedValueOnce({
128152
status: 201,
129153
data: { id: newTeamId, slug: "test-team" },
@@ -142,7 +166,9 @@ describe("createGithubTeam", () => {
142166
});
143167

144168
it("should throw GithubError if team creation fails with non-201 status", async () => {
145-
mockOctokit.request.mockResolvedValueOnce({ data: [] });
169+
mockOctokit.paginate.iterator.mockReturnValue(
170+
createAsyncIterator([{ data: [] }])
171+
);
146172
mockOctokit.request.mockResolvedValueOnce({
147173
status: 400,
148174
data: { message: "Bad request" },
@@ -153,14 +179,26 @@ describe("createGithubTeam", () => {
153179
});
154180

155181
it("should rethrow BaseError instances", async () => {
156-
const baseError = new GithubError({ message: "Failed to create GitHub team." });
157-
mockOctokit.request.mockRejectedValueOnce(baseError);
182+
const baseError = new GithubError({
183+
message: "Failed to create GitHub team.",
184+
});
185+
mockOctokit.paginate.iterator.mockReturnValue(
186+
(async function* () {
187+
yield { data: [] };
188+
throw baseError;
189+
})()
190+
);
158191

159192
await expect(createGithubTeam(defaultInputs)).rejects.toThrow(baseError);
160193
});
161194

162195
it("should wrap non-BaseError exceptions in GithubError", async () => {
163-
mockOctokit.request.mockRejectedValueOnce(new Error("Unknown error"));
196+
mockOctokit.paginate.iterator.mockReturnValue(
197+
(async function* () {
198+
yield { data: [] };
199+
throw new Error("Unknown error");
200+
})()
201+
);
164202

165203
await expect(createGithubTeam(defaultInputs)).rejects.toThrow(GithubError);
166204
expect(mockLogger.error).toHaveBeenCalledWith(
@@ -180,7 +218,7 @@ describe("assignIdpGroupsToTeam", () => {
180218
auth: {
181219
appId: 1,
182220
installationId: 1,
183-
privateKey: "abc"
221+
privateKey: "abc",
184222
},
185223
teamId: 123,
186224
groupsToSync: ["group-1", "group-2"],
@@ -195,8 +233,13 @@ describe("assignIdpGroupsToTeam", () => {
195233
vi.clearAllMocks();
196234
mockOctokit = {
197235
request: vi.fn(() => {}),
236+
paginate: {
237+
iterator: vi.fn(),
238+
},
198239
};
199-
(Octokit as any).mockImplementation(function() { return mockOctokit; });
240+
(Octokit as any).mockImplementation(function () {
241+
return mockOctokit;
242+
});
200243
(utils.sleep as any).mockResolvedValue(undefined);
201244
});
202245

@@ -349,7 +392,9 @@ describe("assignIdpGroupsToTeam", () => {
349392
});
350393

351394
it("should rethrow BaseError instances", async () => {
352-
const baseError = new GithubError({ message: "Failed to assign IdP groups to team 123" });
395+
const baseError = new GithubError({
396+
message: "Failed to assign IdP groups to team 123",
397+
});
353398
mockOctokit.request.mockRejectedValueOnce(baseError);
354399

355400
await expect(assignIdpGroupsToTeam(defaultInputs)).rejects.toThrow(
@@ -392,7 +437,9 @@ describe("assignIdpGroupsToTeam", () => {
392437
expect(mockLogger.warn).toHaveBeenCalledWith(
393438
expect.stringContaining("Team sync is not available for team 123")
394439
);
395-
expect(mockLogger.warn).toHaveBeenCalledWith("Skipping IdP group assignment");
440+
expect(mockLogger.warn).toHaveBeenCalledWith(
441+
"Skipping IdP group assignment"
442+
);
396443
// Should not attempt to search for groups or patch
397444
expect(mockOctokit.request).toHaveBeenCalledTimes(1); // Only sync check
398445
});

0 commit comments

Comments
 (0)