From c49982b9b1415d46d0c97739c55046553d8ac2f0 Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Tue, 29 Oct 2024 18:31:21 +0900 Subject: [PATCH 01/11] fix: using graphql to fetch prs --- src/utils/get-pull-requests.ts | 141 +++++++++++++++++++++++++++++++++ src/utils/issue.ts | 45 ++++++----- 2 files changed, 164 insertions(+), 22 deletions(-) create mode 100644 src/utils/get-pull-requests.ts diff --git a/src/utils/get-pull-requests.ts b/src/utils/get-pull-requests.ts new file mode 100644 index 00000000..95f369f7 --- /dev/null +++ b/src/utils/get-pull-requests.ts @@ -0,0 +1,141 @@ +import { Organization, PullRequest, PullRequestState, Repository } from "@octokit/graphql-schema"; +import { Context } from "../types"; + +type QueryResponse = { + organization: Pick & { + repositories: { + nodes: Array< + Pick & { + pullRequests: { + nodes: Array< + Pick & { + author: { + login: string; + } | null; + } + >; + pageInfo: { + endCursor: string | null; + hasNextPage: boolean; + }; + }; + } + >; + pageInfo: { + endCursor: string | null; + hasNextPage: boolean; + }; + }; + }; +}; + +interface TransformedPullRequest { + repository: string; + number: number; + title: string; + url: string; + author: string | null; + createdAt: string; +} + +interface FetchPullRequestsParams { + context: Context; + organization: string; + state?: PullRequestState[]; +} + +const QUERY_PULL_REQUESTS = /* GraphQL */ ` + query ($organization: String!, $state: [PullRequestState!]!, $repoAfter: String, $prAfter: String) { + organization(login: $organization) { + repositories(first: 100, after: $repoAfter) { + nodes { + name + pullRequests(states: $state, first: 100, after: $prAfter) { + nodes { + number + title + url + author { + login + } + createdAt + } + pageInfo { + endCursor + hasNextPage + } + } + } + pageInfo { + endCursor + hasNextPage + } + } + } + } +`; + +async function getAllPullRequests({ context, organization, state = ["OPEN"] }: FetchPullRequestsParams): Promise { + const { octokit } = context; + const allPullRequests: TransformedPullRequest[] = []; + let hasNextRepoPage = true; + let repoAfter: string | null = null; + + while (hasNextRepoPage) { + try { + const response = (await octokit.graphql(QUERY_PULL_REQUESTS, { + organization, + state, + repoAfter, + prAfter: null, + })) as QueryResponse; + + const { repositories } = response.organization; + + for (const repo of repositories.nodes) { + let hasNextPrPage = true; + let prAfter: string | null = null; + + while (hasNextPrPage) { + const prResponse = (await octokit.graphql(QUERY_PULL_REQUESTS, { + organization, + state, + repoAfter, + prAfter, + })) as QueryResponse; + + const currentRepo = prResponse.organization.repositories.nodes.find((r) => r?.name === repo.name); + + if (currentRepo && currentRepo.pullRequests.nodes?.length) { + const transformedPrs = (currentRepo.pullRequests.nodes.filter((o) => o) as PullRequest[]).map((pr) => ({ + repository: repo.name, + number: pr.number, + title: pr.title, + url: pr.url, + author: pr.author?.login ?? null, + createdAt: pr.createdAt, + })); + + allPullRequests.push(...transformedPrs); + } + + hasNextPrPage = currentRepo?.pullRequests.pageInfo.hasNextPage ?? false; + prAfter = currentRepo?.pullRequests.pageInfo.endCursor ?? null; + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } + + hasNextRepoPage = repositories.pageInfo.hasNextPage; + repoAfter = repositories.pageInfo.endCursor; + } catch (error) { + console.error("Error fetching data:", error); + throw error; + } + } + + return allPullRequests; +} + +export { getAllPullRequests }; +export type { FetchPullRequestsParams, TransformedPullRequest }; diff --git a/src/utils/issue.ts b/src/utils/issue.ts index 65a094e6..84c90dc3 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -1,8 +1,8 @@ -import { RestEndpointMethodTypes } from "@octokit/rest"; import ms from "ms"; import { Context } from "../types/context"; import { GitHubIssueSearch, Review } from "../types/payload"; import { getLinkedPullRequests, GetLinkedResults } from "./get-linked-prs"; +import { getAllPullRequests, TransformedPullRequest } from "./get-pull-requests"; export function isParentIssue(body: string) { const parentPattern = /-\s+\[( |x)\]\s+#\d+/; @@ -170,21 +170,21 @@ export async function addAssignees(context: Context, issueNo: number, assignees: await confirmMultiAssignment(context, issueNo, assignees); } -export async function getAllPullRequests(context: Context, state: "open" | "closed" | "all" = "open", username: string) { - const { payload } = context; - const query: RestEndpointMethodTypes["search"]["issuesAndPullRequests"]["parameters"] = { - q: `org:${payload.repository.owner.login} author:${username} state:${state}`, - per_page: 100, - order: "desc", - sort: "created", - }; - - try { - return (await context.octokit.paginate(context.octokit.search.issuesAndPullRequests, query)) as GitHubIssueSearch["items"]; - } catch (err: unknown) { - throw new Error(context.logger.error("Fetching all pull requests failed!", { error: err as Error, query }).logMessage.raw); - } -} +// export async function getAllPullRequests(context: Context, state: "open" | "closed" | "all" = "open", username: string) { +// const { payload } = context; +// const query: RestEndpointMethodTypes["search"]["issuesAndPullRequests"]["parameters"] = { +// q: `org:${payload.repository.owner.login} author:${username} state:${state}`, +// per_page: 100, +// order: "desc", +// sort: "created", +// }; +// +// try { +// return (await context.octokit.paginate(context.octokit.search.issuesAndPullRequests, query)) as GitHubIssueSearch["items"]; +// } catch (err: unknown) { +// throw new Error(context.logger.error("Fetching all pull requests failed!", { error: err as Error, query }).logMessage.raw); +// } +// } export async function getAllPullRequestReviews(context: Context, pullNumber: number, owner: string, repo: string) { const { @@ -220,11 +220,12 @@ export async function getAvailableOpenedPullRequests(context: Context, username: if (!reviewDelayTolerance) return []; const openedPullRequests = await getOpenedPullRequests(context, username); - const result = [] as typeof openedPullRequests; + const result: TransformedPullRequest[] = []; - for (let i = 0; i < openedPullRequests.length; i++) { + for (let i = 0; openedPullRequests && i < openedPullRequests.length; i++) { const openedPullRequest = openedPullRequests[i]; - const { owner, repo } = getOwnerRepoFromHtmlUrl(openedPullRequest.html_url); + if (!openedPullRequest) continue; + const { owner, repo } = getOwnerRepoFromHtmlUrl(openedPullRequest.url); const reviews = await getAllPullRequestReviews(context, openedPullRequest.number, owner, repo); if (reviews.length > 0) { @@ -234,7 +235,7 @@ export async function getAvailableOpenedPullRequests(context: Context, username: } } - if (reviews.length === 0 && new Date().getTime() - new Date(openedPullRequest.created_at).getTime() >= getTimeValue(reviewDelayTolerance)) { + if (reviews.length === 0 && new Date().getTime() - new Date(openedPullRequest.createdAt).getTime() >= getTimeValue(reviewDelayTolerance)) { result.push(openedPullRequest); } } @@ -252,8 +253,8 @@ export function getTimeValue(timeString: string): number { } async function getOpenedPullRequests(context: Context, username: string): Promise> { - const prs = await getAllPullRequests(context, "open", username); - return prs.filter((pr) => pr.pull_request && pr.state === "open"); + const prs = await getAllPullRequests({ context, state: ["OPEN"], organization: context.payload.repository.owner.login }); + return prs.filter((pr) => pr.author === username); } /** From 995e6be7b8fdac2d720892ebb1cf605f8f14338c Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Tue, 29 Oct 2024 18:43:39 +0900 Subject: [PATCH 02/11] chore: changed wrangler port --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a3121d2f..699149f3 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "knip-ci": "knip --no-exit-code --reporter json --config .github/knip.ts", "prepare": "husky install", "test": "jest --setupFiles dotenv/config --coverage", - "worker": "wrangler dev --env dev --port 4001" + "worker": "wrangler dev --env dev --port 4000" }, "keywords": [ "typescript", From 91a375eaa5e570cba87d950ae609cdffd26b8aff Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Tue, 29 Oct 2024 19:22:43 +0900 Subject: [PATCH 03/11] chore: http request for test --- .gitignore | 3 +- src/plugin.ts | 4 +- tests/http/run.http | 117 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 tests/http/run.http diff --git a/.gitignore b/.gitignore index e23b1058..85ebe090 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ junit.xml cypress/screenshots script.ts .wrangler -test-dashboard.md \ No newline at end of file +test-dashboard.md +*.env.json \ No newline at end of file diff --git a/src/plugin.ts b/src/plugin.ts index bedf2be7..f34f8561 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -31,9 +31,9 @@ export async function startStopTask(inputs: PluginInputs, env: Env) { case "issues.assigned": return await userSelfAssign(context as Context<"issues.assigned">); case "pull_request.opened": - return await userPullRequest(context as Context<"pull_request.edited">); - case "pull_request.edited": return await userPullRequest(context as Context<"pull_request.opened">); + case "pull_request.edited": + return await userPullRequest(context as Context<"pull_request.edited">); case "issues.unassigned": return await userUnassigned(context); default: diff --git a/tests/http/run.http b/tests/http/run.http new file mode 100644 index 00000000..b523c225 --- /dev/null +++ b/tests/http/run.http @@ -0,0 +1,117 @@ +@baseUrl = http://localhost:4000 + +### +# @name mockPullRequestEvent +POST {{baseUrl}} +Content-Type: application/json +X-GitHub-Event: pull_request +X-Hub-Signature-256: sha256=mock_signature +X-GitHub-Delivery: mock-delivery-id + +{ + "action": "created", + "eventName": "issue_comment.created", + "authToken": "{{GITHUB_TOKEN}}", + "eventPayload": { + "issue": { + "url": "https://api.github.com/repos/ubiquity/work.ubq.fi/issues/119", + "repository_url": "https://api.github.com/repos/ubiquity/work.ubq.fi", + "labels_url": "https://api.github.com/repos/ubiquity/work.ubq.fi/issues/119/labels{/name}", + "comments_url": "https://api.github.com/repos/ubiquity/work.ubq.fi/issues/119/comments", + "events_url": "https://api.github.com/repos/ubiquity/work.ubq.fi/issues/119/events", + "html_url": "https://github.com/ubiquity/work.ubq.fi/issues/119", + "id": 12345678, + "node_id": "I_kwDOA1234567", + "number": 119, + "title": "Sample Issue Title", + "user": { + "login": "sshivaditya2019", + "id": 12345678, + "node_id": "MDQ6VXNlcjEyMzQ1Njc4", + "avatar_url": "https://avatars.githubusercontent.com/u/12345678?v=4", + "url": "https://api.github.com/users/sshivaditya2019", + "html_url": "https://github.com/sshivaditya2019", + "type": "User", + "site_admin": false + }, + "state": "open", + "locked": false, + "assignee": null, + "assignees": [], + "milestone": null, + "comments": 1, + "created_at": "2024-03-15T10:00:00Z", + "updated_at": "2024-03-15T10:05:00Z", + "closed_at": null, + "body": "Original issue description" + }, + "comment": { + "url": "https://api.github.com/repos/ubiquity/work.ubq.fi/issues/comments/1234567890", + "html_url": "https://github.com/ubiquity/work.ubq.fi/issues/119#issuecomment-1234567890", + "issue_url": "https://api.github.com/repos/ubiquity/work.ubq.fi/issues/119", + "id": 1234567890, + "node_id": "IC_kwDOA1234567", + "user": { + "login": "sshivaditya2019", + "id": 12345678, + "node_id": "MDQ6VXNlcjEyMzQ1Njc4", + "avatar_url": "https://avatars.githubusercontent.com/u/12345678?v=4", + "url": "https://api.github.com/users/sshivaditya2019", + "html_url": "https://github.com/sshivaditya2019", + "type": "User", + "site_admin": false + }, + "created_at": "2024-03-15T10:05:00Z", + "updated_at": "2024-03-15T10:05:00Z", + "body": "/start" + }, + "repository": { + "id": 98765432, + "node_id": "R_kgDOBcDEFG", + "name": "work.ubq.fi", + "full_name": "ubiquity/work.ubq.fi", + "private": false, + "owner": { + "login": "ubiquity", + "id": 87654321, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjg3NjU0MzIx", + "avatar_url": "https://avatars.githubusercontent.com/u/87654321?v=4", + "url": "https://api.github.com/users/ubiquity", + "html_url": "https://github.com/ubiquity", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/ubiquity/work.ubq.fi", + "description": "Work portal for Ubiquity DAO", + "fork": false, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-03-15T10:00:00Z", + "pushed_at": "2024-03-15T10:00:00Z", + "default_branch": "development" + }, + "organization": { + "login": "ubiquity", + "id": 87654321, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjg3NjU0MzIx", + "url": "https://api.github.com/orgs/ubiquity", + "repos_url": "https://api.github.com/orgs/ubiquity/repos", + "events_url": "https://api.github.com/orgs/ubiquity/events", + "hooks_url": "https://api.github.com/orgs/ubiquity/hooks", + "issues_url": "https://api.github.com/orgs/ubiquity/issues", + "members_url": "https://api.github.com/orgs/ubiquity/members{/member}", + "public_members_url": "https://api.github.com/orgs/ubiquity/public_members{/member}", + "avatar_url": "https://avatars.githubusercontent.com/u/87654321?v=4", + "description": "Ubiquity Organization" + }, + "sender": { + "login": "sshivaditya2019", + "id": 12345678, + "node_id": "MDQ6VXNlcjEyMzQ1Njc4", + "avatar_url": "https://avatars.githubusercontent.com/u/12345678?v=4", + "url": "https://api.github.com/users/sshivaditya2019", + "html_url": "https://github.com/sshivaditya2019", + "type": "User", + "site_admin": false + } +} +} \ No newline at end of file From fd6529974050a330e6f4071f0b4f671d2c3983fe Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Tue, 29 Oct 2024 21:21:42 +0900 Subject: [PATCH 04/11] chore: prs fetch (WIP) --- src/prs.ts | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 src/prs.ts diff --git a/src/prs.ts b/src/prs.ts new file mode 100644 index 00000000..e08450f6 --- /dev/null +++ b/src/prs.ts @@ -0,0 +1,173 @@ +import { Octokit } from "@octokit/rest"; +import dotenv from "dotenv"; +dotenv.config(); +interface PrDetails { + repository: string; + number: number; + title: string; + url: string; + created_at: string; + updated_at: string; +} + +interface PrCountResult { + totalPRs: number; + processedRepos: number; + totalRepos: number; +} + +interface PrDetailsResult { + prs: PrDetails[]; + total: number; + processedRepos: number; + totalRepos: number; +} + +function isHttpError(error: unknown): error is { status: number; message: string } { + return typeof error === "object" && error !== null && "status" in error && "message" in error; +} + +async function countPrivateUserOpenPrs(organization: string, username: string, authToken: string): Promise { + const octokit = new Octokit({ auth: authToken }); + + try { + const repos = await octokit.paginate(octokit.repos.listForOrg, { + org: organization, + per_page: 100, + type: "all", + }); + + let totalPrs = 0; + let processedRepos = 0; + const totalRepos = repos.length; + + const tasks = repos.map(async (repo) => { + processedRepos++; + + if (processedRepos % 10 === 0) { + console.log(`Processing repositories: ${processedRepos}/${totalRepos}`); + } + + try { + const prs = await octokit.paginate(octokit.pulls.list, { + owner: organization, + repo: repo.name, + state: "open", + per_page: 100, + }); + + const userPrs = prs.filter((pr) => pr.user?.login === username); + totalPrs += userPrs.length; + } catch (error) { + if (isHttpError(error)) { + if (error.status === 404) { + console.warn(`Repository ${repo.name} not found or no access`); + return; + } + if (error.status === 403) { + console.warn(`No permission to access ${repo.name}`); + return; + } + } + throw error; + } + }); + + await Promise.all(tasks); + + return { + totalPRs: totalPrs, + processedRepos, + totalRepos, + }; + } catch (error) { + if (isHttpError(error) && error.status === 403) { + throw new Error(`Authentication failed or rate limit exceeded: ${error.message}`); + } + throw error; + } +} + +async function getPrivateUserOpenPrsDetails(organization: string, username: string, authToken: string): Promise { + const octokit = new Octokit({ auth: authToken }); + + try { + const repos = await octokit.paginate(octokit.repos.listForOrg, { + org: organization, + per_page: 100, + type: "all", + }); + + const allPrs: PrDetails[] = []; + let processedRepos = 0; + const totalRepos = repos.length; + + const tasks = repos.map(async (repo) => { + processedRepos++; + + if (processedRepos % 10 === 0) { + console.log(`Processing repositories: ${processedRepos}/${totalRepos}`); + } + + try { + const prs = await octokit.paginate(octokit.pulls.list, { + owner: organization, + repo: repo.name, + state: "open", + per_page: 100, + }); + + const userPrs = prs + .filter((pr) => pr.user?.login === username) + .map((pr) => ({ + repository: repo.name, + number: pr.number, + title: pr.title, + url: pr.html_url, + created_at: pr.created_at, + updated_at: pr.updated_at, + })); + + allPrs.push(...userPrs); + } catch (error) { + if (isHttpError(error) && (error.status === 404 || error.status === 403)) { + return; + } + throw error; + } + }); + + await Promise.all(tasks); + + return { + prs: allPrs, + total: allPrs.length, + processedRepos, + totalRepos, + }; + } catch (error) { + if (isHttpError(error) && error.status === 403) { + throw new Error(`Authentication failed or rate limit exceeded: ${error.message}`); + } + throw error; + } +} + +async function main() { + const organization = "ubiquity"; + const username = "sshivaditya2019"; + const authToken = process.env.GITHUB_TOKEN ?? ""; + + try { + const countResult = await countPrivateUserOpenPrs(organization, username, authToken); + console.log(`Found ${countResult.totalPRs} open PRs by ${username}`); + console.log(`Processed ${countResult.processedRepos} out of ${countResult.totalRepos} repositories`); + + const detailedResult = await getPrivateUserOpenPrsDetails(organization, username, authToken); + console.log(detailedResult); + } catch (error) { + console.error(error); + } +} + +main().catch(console.error); From 3f535b2dae846104852025100bb832bf4535588d Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Tue, 29 Oct 2024 22:41:57 +0900 Subject: [PATCH 05/11] refactor: remove prs.ts and update pull request fetching Removed `prs.ts` and consolidated pull request fetching. --- src/prs.ts | 173 -------------------------------- src/utils/get-pull-requests.ts | 178 +++++++++------------------------ src/utils/issue.ts | 60 ++++++----- 3 files changed, 84 insertions(+), 327 deletions(-) delete mode 100644 src/prs.ts diff --git a/src/prs.ts b/src/prs.ts deleted file mode 100644 index e08450f6..00000000 --- a/src/prs.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Octokit } from "@octokit/rest"; -import dotenv from "dotenv"; -dotenv.config(); -interface PrDetails { - repository: string; - number: number; - title: string; - url: string; - created_at: string; - updated_at: string; -} - -interface PrCountResult { - totalPRs: number; - processedRepos: number; - totalRepos: number; -} - -interface PrDetailsResult { - prs: PrDetails[]; - total: number; - processedRepos: number; - totalRepos: number; -} - -function isHttpError(error: unknown): error is { status: number; message: string } { - return typeof error === "object" && error !== null && "status" in error && "message" in error; -} - -async function countPrivateUserOpenPrs(organization: string, username: string, authToken: string): Promise { - const octokit = new Octokit({ auth: authToken }); - - try { - const repos = await octokit.paginate(octokit.repos.listForOrg, { - org: organization, - per_page: 100, - type: "all", - }); - - let totalPrs = 0; - let processedRepos = 0; - const totalRepos = repos.length; - - const tasks = repos.map(async (repo) => { - processedRepos++; - - if (processedRepos % 10 === 0) { - console.log(`Processing repositories: ${processedRepos}/${totalRepos}`); - } - - try { - const prs = await octokit.paginate(octokit.pulls.list, { - owner: organization, - repo: repo.name, - state: "open", - per_page: 100, - }); - - const userPrs = prs.filter((pr) => pr.user?.login === username); - totalPrs += userPrs.length; - } catch (error) { - if (isHttpError(error)) { - if (error.status === 404) { - console.warn(`Repository ${repo.name} not found or no access`); - return; - } - if (error.status === 403) { - console.warn(`No permission to access ${repo.name}`); - return; - } - } - throw error; - } - }); - - await Promise.all(tasks); - - return { - totalPRs: totalPrs, - processedRepos, - totalRepos, - }; - } catch (error) { - if (isHttpError(error) && error.status === 403) { - throw new Error(`Authentication failed or rate limit exceeded: ${error.message}`); - } - throw error; - } -} - -async function getPrivateUserOpenPrsDetails(organization: string, username: string, authToken: string): Promise { - const octokit = new Octokit({ auth: authToken }); - - try { - const repos = await octokit.paginate(octokit.repos.listForOrg, { - org: organization, - per_page: 100, - type: "all", - }); - - const allPrs: PrDetails[] = []; - let processedRepos = 0; - const totalRepos = repos.length; - - const tasks = repos.map(async (repo) => { - processedRepos++; - - if (processedRepos % 10 === 0) { - console.log(`Processing repositories: ${processedRepos}/${totalRepos}`); - } - - try { - const prs = await octokit.paginate(octokit.pulls.list, { - owner: organization, - repo: repo.name, - state: "open", - per_page: 100, - }); - - const userPrs = prs - .filter((pr) => pr.user?.login === username) - .map((pr) => ({ - repository: repo.name, - number: pr.number, - title: pr.title, - url: pr.html_url, - created_at: pr.created_at, - updated_at: pr.updated_at, - })); - - allPrs.push(...userPrs); - } catch (error) { - if (isHttpError(error) && (error.status === 404 || error.status === 403)) { - return; - } - throw error; - } - }); - - await Promise.all(tasks); - - return { - prs: allPrs, - total: allPrs.length, - processedRepos, - totalRepos, - }; - } catch (error) { - if (isHttpError(error) && error.status === 403) { - throw new Error(`Authentication failed or rate limit exceeded: ${error.message}`); - } - throw error; - } -} - -async function main() { - const organization = "ubiquity"; - const username = "sshivaditya2019"; - const authToken = process.env.GITHUB_TOKEN ?? ""; - - try { - const countResult = await countPrivateUserOpenPrs(organization, username, authToken); - console.log(`Found ${countResult.totalPRs} open PRs by ${username}`); - console.log(`Processed ${countResult.processedRepos} out of ${countResult.totalRepos} repositories`); - - const detailedResult = await getPrivateUserOpenPrsDetails(organization, username, authToken); - console.log(detailedResult); - } catch (error) { - console.error(error); - } -} - -main().catch(console.error); diff --git a/src/utils/get-pull-requests.ts b/src/utils/get-pull-requests.ts index 95f369f7..b75131f0 100644 --- a/src/utils/get-pull-requests.ts +++ b/src/utils/get-pull-requests.ts @@ -1,141 +1,57 @@ -import { Organization, PullRequest, PullRequestState, Repository } from "@octokit/graphql-schema"; +import { RestEndpointMethodTypes } from "@octokit/rest"; +import type { Endpoints } from "@octokit/types"; import { Context } from "../types"; -type QueryResponse = { - organization: Pick & { - repositories: { - nodes: Array< - Pick & { - pullRequests: { - nodes: Array< - Pick & { - author: { - login: string; - } | null; - } - >; - pageInfo: { - endCursor: string | null; - hasNextPage: boolean; - }; - }; - } - >; - pageInfo: { - endCursor: string | null; - hasNextPage: boolean; - }; - }; - }; -}; - -interface TransformedPullRequest { - repository: string; - number: number; - title: string; - url: string; - author: string | null; - createdAt: string; +function isHttpError(error: unknown): error is { status: number; message: string } { + return typeof error === "object" && error !== null && "status" in error && "message" in error; } -interface FetchPullRequestsParams { - context: Context; - organization: string; - state?: PullRequestState[]; -} - -const QUERY_PULL_REQUESTS = /* GraphQL */ ` - query ($organization: String!, $state: [PullRequestState!]!, $repoAfter: String, $prAfter: String) { - organization(login: $organization) { - repositories(first: 100, after: $repoAfter) { - nodes { - name - pullRequests(states: $state, first: 100, after: $prAfter) { - nodes { - number - title - url - author { - login - } - createdAt - } - pageInfo { - endCursor - hasNextPage - } - } - } - pageInfo { - endCursor - hasNextPage +/** + * Fetches all open pull requests within a specified organization created by a particular user. + * This method is slower than using a search query, but should work even if the user has his activity set to private. + */ +export async function getAllPullRequestsFromApi( + context: Context, + state: Endpoints["GET /repos/{owner}/{repo}/pulls"]["parameters"]["state"], + username: string +) { + const { octokit, logger } = context; + const organization = context.payload.repository.owner.login; + + try { + const repos = await octokit.paginate(octokit.repos.listForOrg, { + org: organization, + per_page: 100, + type: "all", + }); + + const allPrs: RestEndpointMethodTypes["pulls"]["list"]["response"]["data"] = []; + + const tasks = repos.map(async (repo) => { + try { + const prs = await octokit.paginate(octokit.pulls.list, { + owner: organization, + repo: repo.name, + state, + per_page: 100, + }); + const userPrs = prs.filter((pr) => pr.user?.login === username); + allPrs.push(...userPrs); + } catch (error) { + if (isHttpError(error) && (error.status === 404 || error.status === 403)) { + logger.error(`Could not find pull requests for repository ${repo.url}, skipping: ${error}`); + return; } + logger.fatal("Failed to fetch pull requests for repository", { error: error as Error }); + throw error; } - } - } -`; - -async function getAllPullRequests({ context, organization, state = ["OPEN"] }: FetchPullRequestsParams): Promise { - const { octokit } = context; - const allPullRequests: TransformedPullRequest[] = []; - let hasNextRepoPage = true; - let repoAfter: string | null = null; - - while (hasNextRepoPage) { - try { - const response = (await octokit.graphql(QUERY_PULL_REQUESTS, { - organization, - state, - repoAfter, - prAfter: null, - })) as QueryResponse; - - const { repositories } = response.organization; - - for (const repo of repositories.nodes) { - let hasNextPrPage = true; - let prAfter: string | null = null; + }); - while (hasNextPrPage) { - const prResponse = (await octokit.graphql(QUERY_PULL_REQUESTS, { - organization, - state, - repoAfter, - prAfter, - })) as QueryResponse; + await Promise.all(tasks); - const currentRepo = prResponse.organization.repositories.nodes.find((r) => r?.name === repo.name); - - if (currentRepo && currentRepo.pullRequests.nodes?.length) { - const transformedPrs = (currentRepo.pullRequests.nodes.filter((o) => o) as PullRequest[]).map((pr) => ({ - repository: repo.name, - number: pr.number, - title: pr.title, - url: pr.url, - author: pr.author?.login ?? null, - createdAt: pr.createdAt, - })); - - allPullRequests.push(...transformedPrs); - } - - hasNextPrPage = currentRepo?.pullRequests.pageInfo.hasNextPage ?? false; - prAfter = currentRepo?.pullRequests.pageInfo.endCursor ?? null; - - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - } - - hasNextRepoPage = repositories.pageInfo.hasNextPage; - repoAfter = repositories.pageInfo.endCursor; - } catch (error) { - console.error("Error fetching data:", error); - throw error; - } + return allPrs; + } catch (error) { + logger.fatal("Failed to fetch pull requests for organization", { error: error as Error }); + throw error; } - - return allPullRequests; } - -export { getAllPullRequests }; -export type { FetchPullRequestsParams, TransformedPullRequest }; diff --git a/src/utils/issue.ts b/src/utils/issue.ts index 84c90dc3..f1c724c6 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -1,8 +1,10 @@ +import { RestEndpointMethodTypes } from "@octokit/rest"; +import { Endpoints } from "@octokit/types"; import ms from "ms"; import { Context } from "../types/context"; import { GitHubIssueSearch, Review } from "../types/payload"; import { getLinkedPullRequests, GetLinkedResults } from "./get-linked-prs"; -import { getAllPullRequests, TransformedPullRequest } from "./get-pull-requests"; +import { getAllPullRequestsFromApi } from "./get-pull-requests"; export function isParentIssue(body: string) { const parentPattern = /-\s+\[( |x)\]\s+#\d+/; @@ -170,21 +172,34 @@ export async function addAssignees(context: Context, issueNo: number, assignees: await confirmMultiAssignment(context, issueNo, assignees); } -// export async function getAllPullRequests(context: Context, state: "open" | "closed" | "all" = "open", username: string) { -// const { payload } = context; -// const query: RestEndpointMethodTypes["search"]["issuesAndPullRequests"]["parameters"] = { -// q: `org:${payload.repository.owner.login} author:${username} state:${state}`, -// per_page: 100, -// order: "desc", -// sort: "created", -// }; -// -// try { -// return (await context.octokit.paginate(context.octokit.search.issuesAndPullRequests, query)) as GitHubIssueSearch["items"]; -// } catch (err: unknown) { -// throw new Error(context.logger.error("Fetching all pull requests failed!", { error: err as Error, query }).logMessage.raw); -// } -// } +async function getAllPullRequests(context: Context, state: Endpoints["GET /repos/{owner}/{repo}/pulls"]["parameters"]["state"] = "open", username: string) { + const { payload } = context; + const query: RestEndpointMethodTypes["search"]["issuesAndPullRequests"]["parameters"] = { + q: `org:${payload.repository.owner.login} author:${username} state:${state}`, + per_page: 100, + order: "desc", + sort: "created", + }; + + try { + return (await context.octokit.paginate(context.octokit.search.issuesAndPullRequests, query)) as GitHubIssueSearch["items"]; + } catch (err: unknown) { + throw new Error(context.logger.error("Fetching all pull requests failed!", { error: err as Error, query }).logMessage.raw); + } +} + +export async function getAllPullRequestsWithRetry( + context: Context, + state: Endpoints["GET /repos/{owner}/{repo}/pulls"]["parameters"]["state"], + username: string +) { + try { + return await getAllPullRequests(context, state, username); + } catch (error) { + context.logger.debug("Will retry fetching all pull requests..."); + return await getAllPullRequestsFromApi(context, state, username); + } +} export async function getAllPullRequestReviews(context: Context, pullNumber: number, owner: string, repo: string) { const { @@ -219,8 +234,8 @@ export async function getAvailableOpenedPullRequests(context: Context, username: const { reviewDelayTolerance } = context.config; if (!reviewDelayTolerance) return []; - const openedPullRequests = await getOpenedPullRequests(context, username); - const result: TransformedPullRequest[] = []; + const openedPullRequests = await getAllPullRequestsWithRetry(context, "open", username); + const result = []; for (let i = 0; openedPullRequests && i < openedPullRequests.length; i++) { const openedPullRequest = openedPullRequests[i]; @@ -235,7 +250,7 @@ export async function getAvailableOpenedPullRequests(context: Context, username: } } - if (reviews.length === 0 && new Date().getTime() - new Date(openedPullRequest.createdAt).getTime() >= getTimeValue(reviewDelayTolerance)) { + if (reviews.length === 0 && new Date().getTime() - new Date(openedPullRequest.created_at).getTime() >= getTimeValue(reviewDelayTolerance)) { result.push(openedPullRequest); } } @@ -252,10 +267,9 @@ export function getTimeValue(timeString: string): number { return timeValue; } -async function getOpenedPullRequests(context: Context, username: string): Promise> { - const prs = await getAllPullRequests({ context, state: ["OPEN"], organization: context.payload.repository.owner.login }); - return prs.filter((pr) => pr.author === username); -} +// async function getOpenedPullRequestsForUser(context: Context, username: string): Promise> { +// return await getAllPullRequests(context, "open", username); +// } /** * Extracts the task id from the PR body. The format is: From cf145f0361ba7c0fcf76c2db620c044f556918af Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Wed, 30 Oct 2024 00:13:25 +0900 Subject: [PATCH 06/11] chore: add @octokit/types dependency Add @octokit/types to package.json and refactor issue utils. --- package.json | 1 + src/utils/issue.ts | 11 ++++++----- tests/http/run.http | 6 +----- yarn.lock | 10 ++++++++++ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 699149f3..3f42970a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@octokit/graphql-schema": "15.25.0", "@octokit/plugin-paginate-graphql": "5.2.2", "@octokit/rest": "20.1.1", + "@octokit/types": "^13.6.1", "@octokit/webhooks": "13.2.7", "@sinclair/typebox": "^0.32.5", "@supabase/supabase-js": "2.42.0", diff --git a/src/utils/issue.ts b/src/utils/issue.ts index f1c724c6..f647532d 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -1,5 +1,6 @@ import { RestEndpointMethodTypes } from "@octokit/rest"; import { Endpoints } from "@octokit/types"; +import { ReturnType } from "@sinclair/typebox"; import ms from "ms"; import { Context } from "../types/context"; import { GitHubIssueSearch, Review } from "../types/payload"; @@ -234,8 +235,8 @@ export async function getAvailableOpenedPullRequests(context: Context, username: const { reviewDelayTolerance } = context.config; if (!reviewDelayTolerance) return []; - const openedPullRequests = await getAllPullRequestsWithRetry(context, "open", username); - const result = []; + const openedPullRequests = await getOpenedPullRequestsForUser(context, username); + const result: Awaited> = []; for (let i = 0; openedPullRequests && i < openedPullRequests.length; i++) { const openedPullRequest = openedPullRequests[i]; @@ -267,9 +268,9 @@ export function getTimeValue(timeString: string): number { return timeValue; } -// async function getOpenedPullRequestsForUser(context: Context, username: string): Promise> { -// return await getAllPullRequests(context, "open", username); -// } +async function getOpenedPullRequestsForUser(context: Context, username: string): Promise> { + return await getAllPullRequests(context, "open", username); +} /** * Extracts the task id from the PR body. The format is: diff --git a/tests/http/run.http b/tests/http/run.http index b523c225..61ddb874 100644 --- a/tests/http/run.http +++ b/tests/http/run.http @@ -1,8 +1,4 @@ -@baseUrl = http://localhost:4000 - -### -# @name mockPullRequestEvent -POST {{baseUrl}} +POST http://localhost:4000 Content-Type: application/json X-GitHub-Event: pull_request X-Hub-Signature-256: sha256=mock_signature diff --git a/yarn.lock b/yarn.lock index d4bd9974..4296dc78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2286,6 +2286,15 @@ __metadata: languageName: node linkType: hard +"@octokit/types@npm:^13.6.1": + version: 13.6.1 + resolution: "@octokit/types@npm:13.6.1" + dependencies: + "@octokit/openapi-types": "npm:^22.2.0" + checksum: 10c0/891334b5786ba6aef953384cec05d53e05132dd577c0c22db124d55eaa69609362d1e3147853b46e91bf226e046ba24d615c55214c8f8f4e7c3a5c38429b38e9 + languageName: node + linkType: hard + "@octokit/webhooks-methods@npm:^5.0.0": version: 5.1.0 resolution: "@octokit/webhooks-methods@npm:5.1.0" @@ -2824,6 +2833,7 @@ __metadata: "@octokit/graphql-schema": "npm:15.25.0" "@octokit/plugin-paginate-graphql": "npm:5.2.2" "@octokit/rest": "npm:20.1.1" + "@octokit/types": "npm:^13.6.1" "@octokit/webhooks": "npm:13.2.7" "@sinclair/typebox": "npm:^0.32.5" "@supabase/supabase-js": "npm:2.42.0" From 742b3e40bea1da6a6360564871fad5f19477514b Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Wed, 30 Oct 2024 00:34:10 +0900 Subject: [PATCH 07/11] refactor: update pull request fetching logic Change debug to info log level and streamline retry mechanism. --- src/utils/issue.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/issue.ts b/src/utils/issue.ts index f647532d..07d5077c 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -197,8 +197,8 @@ export async function getAllPullRequestsWithRetry( try { return await getAllPullRequests(context, state, username); } catch (error) { - context.logger.debug("Will retry fetching all pull requests..."); - return await getAllPullRequestsFromApi(context, state, username); + context.logger.info("Will retry re-fetching all pull requests..."); + return getAllPullRequestsFromApi(context, state, username); } } @@ -268,8 +268,8 @@ export function getTimeValue(timeString: string): number { return timeValue; } -async function getOpenedPullRequestsForUser(context: Context, username: string): Promise> { - return await getAllPullRequests(context, "open", username); +async function getOpenedPullRequestsForUser(context: Context, username: string): Promise> { + return getAllPullRequestsWithRetry(context, "open", username); } /** From 16ba3a84835b5fc74d882fb4f0f5902491eec57a Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Wed, 30 Oct 2024 01:40:59 +0900 Subject: [PATCH 08/11] fix: add fallback methods for fetching issues/PRs Refactor code to use fallback methods for better error handling. --- ...uests.ts => get-pull-requests-fallback.ts} | 40 +++++++++++++++++-- src/utils/issue.ts | 18 ++++----- 2 files changed, 46 insertions(+), 12 deletions(-) rename src/utils/{get-pull-requests.ts => get-pull-requests-fallback.ts} (59%) diff --git a/src/utils/get-pull-requests.ts b/src/utils/get-pull-requests-fallback.ts similarity index 59% rename from src/utils/get-pull-requests.ts rename to src/utils/get-pull-requests-fallback.ts index b75131f0..3766ba92 100644 --- a/src/utils/get-pull-requests.ts +++ b/src/utils/get-pull-requests-fallback.ts @@ -10,7 +10,7 @@ function isHttpError(error: unknown): error is { status: number; message: string * Fetches all open pull requests within a specified organization created by a particular user. * This method is slower than using a search query, but should work even if the user has his activity set to private. */ -export async function getAllPullRequestsFromApi( +export async function getAllPullRequestsFallback( context: Context, state: Endpoints["GET /repos/{owner}/{repo}/pulls"]["parameters"]["state"], username: string @@ -19,7 +19,7 @@ export async function getAllPullRequestsFromApi( const organization = context.payload.repository.owner.login; try { - const repos = await octokit.paginate(octokit.repos.listForOrg, { + const repositories = await octokit.paginate(octokit.repos.listForOrg, { org: organization, per_page: 100, type: "all", @@ -27,7 +27,7 @@ export async function getAllPullRequestsFromApi( const allPrs: RestEndpointMethodTypes["pulls"]["list"]["response"]["data"] = []; - const tasks = repos.map(async (repo) => { + const tasks = repositories.map(async (repo) => { try { const prs = await octokit.paginate(octokit.pulls.list, { owner: organization, @@ -55,3 +55,37 @@ export async function getAllPullRequestsFromApi( throw error; } } + +export async function getAssignedIssuesFallback(context: Context, username: string) { + const org = context.payload.repository.owner.login; + const assignedIssues = []; + + try { + const repositories = await context.octokit.paginate(context.octokit.rest.repos.listForOrg, { + org, + type: "all", + per_page: 100, + }); + + for (const repo of repositories) { + const issues = await context.octokit.paginate(context.octokit.rest.issues.listForRepo, { + owner: org, + repo: repo.name, + assignee: username, + state: "open", + per_page: 100, + }); + + assignedIssues.push( + ...issues.filter( + (issue) => + issue.pull_request === undefined && (issue.assignee?.login === username || issue.assignees?.some((assignee) => assignee.login === username)) + ) + ); + } + + return assignedIssues; + } catch (err: unknown) { + throw new Error(context.logger.error("Fetching assigned issues failed!", { error: err as Error }).logMessage.raw); + } +} diff --git a/src/utils/issue.ts b/src/utils/issue.ts index 07d5077c..7a70a91b 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -1,18 +1,17 @@ import { RestEndpointMethodTypes } from "@octokit/rest"; import { Endpoints } from "@octokit/types"; -import { ReturnType } from "@sinclair/typebox"; import ms from "ms"; import { Context } from "../types/context"; import { GitHubIssueSearch, Review } from "../types/payload"; import { getLinkedPullRequests, GetLinkedResults } from "./get-linked-prs"; -import { getAllPullRequestsFromApi } from "./get-pull-requests"; +import { getAllPullRequestsFallback, getAssignedIssuesFallback } from "./get-pull-requests-fallback"; export function isParentIssue(body: string) { const parentPattern = /-\s+\[( |x)\]\s+#\d+/; return body.match(parentPattern); } -export async function getAssignedIssues(context: Context, username: string): Promise { +export async function getAssignedIssues(context: Context, username: string) { const payload = context.payload; try { @@ -28,8 +27,9 @@ export async function getAssignedIssues(context: Context, username: string): Pro return issue.state === "open" && (issue.assignee?.login === username || issue.assignees?.some((assignee) => assignee.login === username)); }) ); - } catch (err: unknown) { - throw new Error(context.logger.error("Fetching assigned issues failed!", { error: err as Error }).logMessage.raw); + } catch (err) { + context.logger.info("Will try re-fetching assigned issues...", { error: err as Error }); + return getAssignedIssuesFallback(context, username); } } @@ -197,8 +197,8 @@ export async function getAllPullRequestsWithRetry( try { return await getAllPullRequests(context, state, username); } catch (error) { - context.logger.info("Will retry re-fetching all pull requests..."); - return getAllPullRequestsFromApi(context, state, username); + context.logger.info("Will retry re-fetching all pull requests...", { error: error as Error }); + return getAllPullRequestsFallback(context, state, username); } } @@ -236,12 +236,12 @@ export async function getAvailableOpenedPullRequests(context: Context, username: if (!reviewDelayTolerance) return []; const openedPullRequests = await getOpenedPullRequestsForUser(context, username); - const result: Awaited> = []; + const result: (typeof openedPullRequests)[number][] = []; for (let i = 0; openedPullRequests && i < openedPullRequests.length; i++) { const openedPullRequest = openedPullRequests[i]; if (!openedPullRequest) continue; - const { owner, repo } = getOwnerRepoFromHtmlUrl(openedPullRequest.url); + const { owner, repo } = getOwnerRepoFromHtmlUrl(openedPullRequest.html_url); const reviews = await getAllPullRequestReviews(context, openedPullRequest.number, owner, repo); if (reviews.length > 0) { From 3d760e37911f24c4d61601bc74960d742396b36b Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Wed, 30 Oct 2024 01:58:58 +0900 Subject: [PATCH 09/11] fix: update octokit method namespace Refactored to use `octokit.rest` namespace for paginate methods. --- src/utils/issue.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/issue.ts b/src/utils/issue.ts index 7a70a91b..c04a1081 100644 --- a/src/utils/issue.ts +++ b/src/utils/issue.ts @@ -16,7 +16,7 @@ export async function getAssignedIssues(context: Context, username: string) { try { return await context.octokit - .paginate(context.octokit.search.issuesAndPullRequests, { + .paginate(context.octokit.rest.search.issuesAndPullRequests, { q: `org:${payload.repository.owner.login} assignee:${username} is:open is:issue`, per_page: 100, order: "desc", @@ -183,7 +183,7 @@ async function getAllPullRequests(context: Context, state: Endpoints["GET /repos }; try { - return (await context.octokit.paginate(context.octokit.search.issuesAndPullRequests, query)) as GitHubIssueSearch["items"]; + return (await context.octokit.paginate(context.octokit.rest.search.issuesAndPullRequests, query)) as GitHubIssueSearch["items"]; } catch (err: unknown) { throw new Error(context.logger.error("Fetching all pull requests failed!", { error: err as Error, query }).logMessage.raw); } @@ -208,7 +208,7 @@ export async function getAllPullRequestReviews(context: Context, pullNumber: num } = context; try { return ( - await context.octokit.paginate(context.octokit.pulls.listReviews, { + await context.octokit.paginate(context.octokit.rest.pulls.listReviews, { owner, repo, pull_number: pullNumber, From f6232c05105fabd3c25e8101550a9017ae6c59b0 Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Wed, 30 Oct 2024 02:20:51 +0900 Subject: [PATCH 10/11] test: add tests for `getAllPullRequestsWithRetry` Introduce mock context and data to validate pull request retrieval. --- src/utils/get-pull-requests-fallback.ts | 4 +- tests/fallbacks.test.ts | 49 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/fallbacks.test.ts diff --git a/src/utils/get-pull-requests-fallback.ts b/src/utils/get-pull-requests-fallback.ts index 3766ba92..73352629 100644 --- a/src/utils/get-pull-requests-fallback.ts +++ b/src/utils/get-pull-requests-fallback.ts @@ -19,7 +19,7 @@ export async function getAllPullRequestsFallback( const organization = context.payload.repository.owner.login; try { - const repositories = await octokit.paginate(octokit.repos.listForOrg, { + const repositories = await octokit.paginate(octokit.rest.repos.listForOrg, { org: organization, per_page: 100, type: "all", @@ -29,7 +29,7 @@ export async function getAllPullRequestsFallback( const tasks = repositories.map(async (repo) => { try { - const prs = await octokit.paginate(octokit.pulls.list, { + const prs = await octokit.paginate(octokit.rest.pulls.list, { owner: organization, repo: repo.name, state, diff --git a/tests/fallbacks.test.ts b/tests/fallbacks.test.ts new file mode 100644 index 00000000..417f3116 --- /dev/null +++ b/tests/fallbacks.test.ts @@ -0,0 +1,49 @@ +import { RestEndpointMethodTypes } from "@octokit/rest"; +import { Logs } from "@ubiquity-os/ubiquity-os-logger"; +import { Context } from "../src/types/context"; +import { getAllPullRequestsWithRetry } from "../src/utils/issue"; + +const username = "private-user"; + +const mockPullRequestData = [ + { id: 1, number: 123, state: "open", user: { login: username } }, + { id: 2, number: 124, state: "open", user: { login: "public-user" } }, +] as unknown as RestEndpointMethodTypes["pulls"]["list"]["response"]["data"]; + +const mockOctokit = { + paginate: jest.fn().mockResolvedValue(mockPullRequestData), + rest: { + pulls: { + list: jest.fn().mockResolvedValue(mockPullRequestData), + }, + repos: { + listForOrg: jest.fn().mockResolvedValue(mockPullRequestData), + }, + }, +}; + +// Mock context +const context: Context = { + eventName: "pull_request", + payload: { + repository: { + name: "test-repo", + owner: { + login: "test-owner", + }, + }, + }, + octokit: mockOctokit as unknown as Context["octokit"], + logger: new Logs("debug"), + adapters: {}, +} as unknown as Context; + +describe("getAllPullRequestsWithRetry", () => { + it("should return pull requests even if user information is private", async () => { + const pullRequests = await getAllPullRequestsWithRetry(context, "all", username); + expect(pullRequests).toHaveLength(2); + expect(pullRequests[0].user?.login).toBe(username); + expect(pullRequests[1].user?.login).toBe(username); + console.log(pullRequests); + }); +}); From 5dd964152be975b9f623ec9dfd8a61894b010519 Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Wed, 30 Oct 2024 02:25:04 +0900 Subject: [PATCH 11/11] test: removed comment --- tests/fallbacks.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/fallbacks.test.ts b/tests/fallbacks.test.ts index 417f3116..e4db27b2 100644 --- a/tests/fallbacks.test.ts +++ b/tests/fallbacks.test.ts @@ -22,7 +22,6 @@ const mockOctokit = { }, }; -// Mock context const context: Context = { eventName: "pull_request", payload: {