From c49982b9b1415d46d0c97739c55046553d8ac2f0 Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Tue, 29 Oct 2024 18:31:21 +0900 Subject: [PATCH] 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 0000000..95f369f --- /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 65a094e..84c90dc 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); } /**