-
Notifications
You must be signed in to change notification settings - Fork 5
fix(gh-design): skip self-review requests and tolerate 422 errors #202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2004175
4cf8723
2c8cfd9
33e8f70
7472a00
d5a2921
f97f018
cee9996
7ab8d77
0cc2d8f
e43396f
29caf1f
add499b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { describe, expect, it } from "bun:test"; | ||
| import { filterReviewers } from "./filterReviewers"; | ||
|
|
||
| describe("filterReviewers", () => { | ||
| const REVIEWERS = ["PabloWiedemann", "AliceDev"]; | ||
|
|
||
| it("excludes the PR author from both lists", () => { | ||
| const result = filterReviewers(REVIEWERS, "PabloWiedemann"); | ||
| expect(result.requestReviewers).toEqual(["AliceDev"]); | ||
| expect(result.newReviewers).toEqual(["AliceDev"]); | ||
| }); | ||
|
|
||
| it("returns all reviewers when author is not in the list", () => { | ||
| const result = filterReviewers(REVIEWERS, "SomeoneElse"); | ||
| expect(result.requestReviewers).toEqual(["PabloWiedemann", "AliceDev"]); | ||
| expect(result.newReviewers).toEqual(["PabloWiedemann", "AliceDev"]); | ||
| }); | ||
|
|
||
| it("excludes already-requested reviewers from newReviewers only", () => { | ||
| const result = filterReviewers(REVIEWERS, "SomeoneElse", ["PabloWiedemann"]); | ||
| expect(result.requestReviewers).toEqual(["PabloWiedemann", "AliceDev"]); | ||
| expect(result.newReviewers).toEqual(["AliceDev"]); | ||
| }); | ||
|
|
||
| it("returns empty newReviewers when all are already requested", () => { | ||
| const result = filterReviewers(REVIEWERS, "SomeoneElse", ["PabloWiedemann", "AliceDev"]); | ||
| expect(result.requestReviewers).toEqual(["PabloWiedemann", "AliceDev"]); | ||
| expect(result.newReviewers).toEqual([]); | ||
| }); | ||
|
|
||
| it("returns empty lists when author is the only reviewer", () => { | ||
| const result = filterReviewers(["PabloWiedemann"], "PabloWiedemann"); | ||
| expect(result.requestReviewers).toEqual([]); | ||
| expect(result.newReviewers).toEqual([]); | ||
| }); | ||
|
|
||
| it("handles undefined alreadyRequested as no-one requested yet", () => { | ||
| const result = filterReviewers(REVIEWERS, "SomeoneElse", undefined); | ||
| expect(result.newReviewers).toEqual(["PabloWiedemann", "AliceDev"]); | ||
| }); | ||
|
|
||
| it("compares usernames case-insensitively", () => { | ||
| const result = filterReviewers(REVIEWERS, "pablowiedemann"); | ||
| expect(result.requestReviewers).toEqual(["AliceDev"]); | ||
| }); | ||
|
|
||
| it("matches already-requested reviewers case-insensitively", () => { | ||
| const result = filterReviewers(REVIEWERS, "SomeoneElse", ["pablowiedemann"]); | ||
| expect(result.newReviewers).toEqual(["AliceDev"]); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| /** | ||
| * Compute eligible reviewers for a PR. | ||
| * Returns `requestReviewers` (all reviewers minus the PR author) and | ||
| * `newReviewers` (eligible reviewers not yet requested). | ||
| * GitHub usernames are case-insensitive, so comparisons are normalized. | ||
| */ | ||
| export function filterReviewers( | ||
| allReviewers: string[], | ||
| prAuthor: string, | ||
| alreadyRequested?: string[], | ||
| ): { requestReviewers: string[]; newReviewers: string[] } { | ||
| const normalizedAuthor = prAuthor.toLowerCase(); | ||
| const normalizedRequested = new Set(alreadyRequested?.map((r) => r.toLowerCase()) ?? []); | ||
| const requestReviewers = allReviewers.filter((e) => e.toLowerCase() !== normalizedAuthor); | ||
| const newReviewers = requestReviewers.filter((e) => !normalizedRequested.has(e.toLowerCase())); | ||
| return { requestReviewers, newReviewers }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ import { | |
| planDesignCommentNotification, | ||
| } from "./slackNotifications"; | ||
| import { slackMessageUrlParse, slackMessageUrlStringify } from "./slackMessageUrlParse"; | ||
| import { filterReviewers } from "./filterReviewers"; | ||
| const tlog = createTimeLogger(); | ||
|
|
||
| /** | ||
|
|
@@ -254,21 +255,37 @@ export async function runGithubDesignTask() { | |
| }); | ||
|
|
||
| if (task.state === "open") { | ||
| if ( | ||
| task.type === "pull_request" && | ||
| REQUEST_REVIEWERS.some((e) => !task.reviewers?.includes(e)) | ||
| ) { | ||
| const requestReviewers = REQUEST_REVIEWERS; | ||
| const newReviewers = requestReviewers.filter((e) => !task.reviewers?.includes(e)); | ||
| tlog(`Requesting reviewers: ${newReviewers.join(", ")}`); | ||
| if (!dryRun) { | ||
| await gh.pulls.requestReviewers({ | ||
| owner, | ||
| repo, | ||
| pull_number: issue_number, | ||
| reviewers: newReviewers, | ||
| }); | ||
| task = await saveGithubDesignTask(url, { reviewers: requestReviewers }); | ||
| if (task.type === "pull_request") { | ||
| const { requestReviewers, newReviewers } = filterReviewers( | ||
| REQUEST_REVIEWERS, | ||
| task.user, | ||
| task.reviewers, | ||
| ); | ||
| if (newReviewers.length > 0) { | ||
| tlog(`Requesting reviewers: ${newReviewers.join(", ")}`); | ||
| if (!dryRun) { | ||
| let reviewersRequested = false; | ||
| try { | ||
| await gh.pulls.requestReviewers({ | ||
| owner, | ||
| repo, | ||
| pull_number: issue_number, | ||
| reviewers: newReviewers, | ||
| }); | ||
| reviewersRequested = true; | ||
| } catch (err: unknown) { | ||
| // GitHub may return 422 when a requested reviewer cannot be added, | ||
| // such as when they are not a collaborator or cannot be requested. | ||
| // We log but don't persist, so the request will be retried on the | ||
| // next run (the reviewer may become eligible later). | ||
| const status = (err as { status?: number })?.status; | ||
| if (status !== 422) throw err; | ||
| tlog(`Reviewer request rejected (422): ${err}`); | ||
| } | ||
|
Comment on lines
+277
to
+284
|
||
| if (reviewersRequested) { | ||
| task = await saveGithubDesignTask(url, { reviewers: requestReviewers }); | ||
| } | ||
|
Comment on lines
+258
to
+287
|
||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test imports
filterReviewersfrom./gh-design, butgh-design.tsimports@/src/dbwhich performs a top-level MongoClient initialization (top-level await) and will try to connect during tests unless@/src/dbis mocked. This can make the unit test suite hang/fail whenMONGODB_URIisn’t set. Consider either movingfilterReviewersinto a small side-effect-free module (e.g.filterReviewers.ts) that both the task and the test import, or change the spec tomock.module("@/src/db", ...)before dynamically importing./gh-design.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in cee9996 — extracted filterReviewers() into its own side-effect-free module (filterReviewers.ts). Test now imports directly from it without triggering any DB initialization. Test time dropped from ~1126ms to ~148ms.