diff --git a/src/handlers/comment/handlers/issue/get-collaborator-ids-for-repo.ts b/src/handlers/comment/handlers/issue/get-collaborator-ids-for-repo.ts index 1b480c8c4..b74b47d35 100644 --- a/src/handlers/comment/handlers/issue/get-collaborator-ids-for-repo.ts +++ b/src/handlers/comment/handlers/issue/get-collaborator-ids-for-repo.ts @@ -1,32 +1,18 @@ import { Context } from "../../../../types/context"; -import { Payload, User } from "../../../../types/payload"; +import { User } from "../../../../types/payload"; export async function getCollaboratorsForRepo(context: Context): Promise { - const payload = context.event.payload as Payload; - const collaboratorUsers: User[] = []; + const payload = context.payload; try { - let page = 1; - let shouldFetch = true; - - while (shouldFetch) { - const res = await context.event.octokit.rest.repos.listCollaborators({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - per_page: 100, - page: page, - }); - - if (res.data.length > 0) { - res.data.forEach((collaborator) => collaboratorUsers.push(collaborator as User)); - page++; - } else { - shouldFetch = false; - } - } - } catch (e: unknown) { - context.logger.error("Fetching collaborator IDs for repo failed!", e); + const collaboratorUsers = (await context.octokit.paginate(context.octokit.rest.repos.listCollaborators, { + owner: payload.repository.owner.login, + repo: payload.repository.name, + per_page: 100, + })) as User[]; + return collaboratorUsers; + } catch (err: unknown) { + context.logger.error("Failed to fetch lists of collaborators", err); + return []; } - - return collaboratorUsers; } diff --git a/src/handlers/comment/handlers/issue/issue-closed.ts b/src/handlers/comment/handlers/issue/issue-closed.ts index 3c6916b91..1f4a32c30 100644 --- a/src/handlers/comment/handlers/issue/issue-closed.ts +++ b/src/handlers/comment/handlers/issue/issue-closed.ts @@ -43,7 +43,7 @@ export async function issueClosed(context: Context) { return "Please wait until we get the result."; } -export async function dispatchWorkflow(owner: string, repo: string, workflowId: string, inputs: any) { +async function dispatchWorkflow(owner: string, repo: string, workflowId: string, inputs: any) { const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflowId}/dispatches`, { method: "POST", headers: { diff --git a/src/handlers/wildcard/unassign/unassign.ts b/src/handlers/wildcard/unassign/unassign.ts index 5cc832233..c1bc15b56 100644 --- a/src/handlers/wildcard/unassign/unassign.ts +++ b/src/handlers/wildcard/unassign/unassign.ts @@ -311,56 +311,41 @@ function getActiveAssigneesInDisqualifyDuration( } async function getAllEvents({ context, owner, repo, issueNumber }: GetAllEvents) { - let allEvents: IssuesListEventsResponseData = []; - let page = 1; - let events = await context.event.octokit.issues.listEvents({ - owner, - repo, - issue_number: issueNumber, - per_page: 100, - page: page, - }); - - while (events.data.length > 0) { - allEvents = allEvents.concat(events.data.filter(isCorrectType) as IssuesListEventsResponseData); - - events = await context.event.octokit.issues.listEvents({ - owner, - repo, - issue_number: issueNumber, - per_page: 100, - page: ++page, - }); + try { + const events = (await context.octokit.paginate( + context.octokit.rest.issues.listEvents, + { + owner, + repo, + issue_number: issueNumber, + per_page: 100, + }, + (response) => response.data.filter((event) => isCorrectType(event as IssuesListEventsResponseData[0])) + )) as IssuesListEventsResponseData; + return events; + } catch (err: unknown) { + context.logger.error("Failed to fetch lists of events", err); + return []; } - return allEvents; } async function getAllCommitsFromPullRequest({ context, owner, repo, pullNumber }: GetAllCommits) { - let allCommits = [] as Commit[]; - let commitPage = 1; - let hasMoreCommits = true; - - while (hasMoreCommits) { - const commits = await context.event.octokit.pulls.listCommits({ + try { + const commits = (await context.octokit.paginate(context.octokit.pulls.listCommits, { owner, repo, pull_number: pullNumber, per_page: 100, - page: commitPage, - }); - - if (commits.data.length === 0) { - hasMoreCommits = false; - } else { - allCommits = allCommits.concat(commits.data as Commit[]); - commitPage++; - } + })) as Commit[]; + return commits; + } catch (err: unknown) { + context.logger.error("Failed to fetch lists of commits", err); + return []; } - return allCommits; } -function isCorrectType(event: any): event is AssignedEvent { - return event && typeof event.id === "number" && event.event === "assigned"; +function isCorrectType(event: IssuesListEventsResponseData[0]) { + return event && typeof event.id === "number"; } interface DisqualifyIdleAssignees { diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts index af25f2982..2dd68554e 100644 --- a/src/helpers/issue.ts +++ b/src/helpers/issue.ts @@ -2,48 +2,21 @@ import { LogReturn } from "../adapters/supabase/helpers/tables/logs"; import { Context } from "../types/context"; import { HandlerReturnValuesNoVoid } from "../types/handlers"; import { StreamlinedComment } from "../types/openai"; -import { AssignEvent, Issue, IssueType, Payload, UserType } from "../types/payload"; -import { Comment } from "../types/payload"; -import { checkRateLimitGit } from "../utils/check-github-rate-limit"; - -type PromiseType = T extends Promise ? U : never; +import { AssignEvent, Comment, Issue, IssueType, Payload, UserType } from "../types/payload"; async function getAllIssueEvents(context: Context) { - type Event = PromiseType>["data"][0]; - - const payload = context.event.payload as Payload; - if (!payload.issue) return; - - let shouldFetch = true; - let page = 1; - - const events = [] as Event[]; + if (!context.payload.issue) return; - while (shouldFetch) { - // Fetch issue events - - const repo = payload.repository.name; - const owner = payload.repository.owner.login; - - const response = await context.event.octokit.issues.listEvents({ - owner: owner, - repo: repo, - issue_number: payload.issue.number, + try { + const events = await context.octokit.paginate(context.octokit.issues.listEvents, { + ...context.event.issue(), per_page: 100, - page: page, }); - - await checkRateLimitGit(response?.headers); - - if (response.data.length > 0) { - events.push(...(response.data as Event[])); - page++; - } else { - shouldFetch = false; - } + return events; + } catch (err: unknown) { + context.logger.error("Failed to fetch lists of events", err); + return []; } - - return events; } export async function getAllLabeledEvents(context: Context) { @@ -53,7 +26,7 @@ export async function getAllLabeledEvents(context: Context) { } export async function clearAllPriceLabelsOnIssue(context: Context) { - const payload = context.event.payload as Payload; + const payload = context.payload; if (!payload.issue) return; const labels = payload.issue.labels; @@ -61,33 +34,29 @@ export async function clearAllPriceLabelsOnIssue(context: Context) { if (!issuePrices.length) return; - // try { - await context.event.octokit.issues.removeLabel({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - issue_number: payload.issue.number, - name: issuePrices[0].name, - }); - // } catch (e: unknown) { - // context.logger.debug("Clearing all price labels failed!", e); - // } + try { + await context.event.octokit.issues.removeLabel({ + ...context.event.issue(), + name: issuePrices[0].name, + }); + } catch (e: unknown) { + context.logger.error("Clearing all price labels failed!", e); + } } export async function addLabelToIssue(context: Context, labelName: string) { - const payload = context.event.payload as Payload; + const payload = context.payload; if (!payload.issue) { throw context.logger.error("Issue object is null"); } try { - await context.event.octokit.issues.addLabels({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - issue_number: payload.issue.number, + await context.octokit.issues.addLabels({ + ...context.event.issue(), labels: [labelName], }); } catch (e: unknown) { - context.logger.debug("Adding a label to issue failed!", e); + context.logger.error("Adding a label to issue failed!", e); } } @@ -99,42 +68,46 @@ async function listIssuesAndPullsForRepo( sort: "created" | "updated" | "comments" = "created", direction: "desc" | "asc" = "desc" ) { - const payload = context.event.payload as Payload; - const response = await context.event.octokit.issues.listForRepo({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - state, - per_page: perPage, - page, - sort, - direction, - }); - - await checkRateLimitGit(response.headers); + const payload = context.payload; - if (response.status === 200) { + try { + const response = await context.octokit.issues.listForRepo({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + state, + per_page: perPage, + page, + sort, + direction, + }); return response.data; - } else { + } catch (err: unknown) { + context.logger.error("Failed to fetch lists of issues", err); return []; } } -export async function listAllIssuesAndPullsForRepo(context: Context, state: "open" | "closed" | "all") { - const issuesArr = [] as Issue[]; - const perPage = 100; - let isFetchDone = false; - let curPage = 1; - while (!isFetchDone) { - const issues = (await listIssuesAndPullsForRepo(context, state, perPage, curPage)) as Issue[]; - - // push the objects to array - issuesArr.push(...issues); - - if (issues.length < perPage) isFetchDone = true; - else curPage++; +export async function listAllIssuesAndPullsForRepo( + context: Context, + state: "open" | "closed" | "all" = "open", + sort: "created" | "updated" | "comments" = "created", + direction: "desc" | "asc" = "desc" +) { + const payload = context.payload; + try { + const issues = (await context.octokit.paginate(context.octokit.issues.listForRepo, { + owner: payload.repository.owner.login, + repo: payload.repository.name, + state, + sort, + direction, + per_page: 100, + })) as Issue[]; + return issues; + } catch (err: unknown) { + context.logger.error("Listing all issues and pulls failed!", err); + return []; } - - return issuesArr; } export async function addCommentToIssue(context: Context, message: HandlerReturnValuesNoVoid, issueNumber: number) { @@ -149,9 +122,9 @@ export async function addCommentToIssue(context: Context, message: HandlerReturn comment = comment.concat(metadataSerializedAsComment); } - const payload = context.event.payload as Payload; + const payload = context.payload; try { - await context.event.octokit.issues.createComment({ + await context.octokit.issues.createComment({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issueNumber, @@ -162,45 +135,92 @@ export async function addCommentToIssue(context: Context, message: HandlerReturn } } +export async function upsertLastCommentToIssue(context: Context, issueNumber: number, commentBody: string) { + try { + const comments = await getAllIssueComments(context, issueNumber); + + if (comments.length > 0 && comments[comments.length - 1].body !== commentBody) + await addCommentToIssue(context, commentBody, issueNumber); + } catch (e: unknown) { + context.logger.error("Upserting last comment failed!", e); + } +} + +export async function getIssueDescription( + context: Context, + issueNumber: number, + format: "raw" | "html" | "text" = "raw" +): Promise { + const payload = context.payload; + + try { + const response = await context.octokit.rest.issues.get({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issueNumber, + mediaType: { + format, + }, + }); + + let result = response.data.body; + + if (format === "html") { + result = response.data.body_html; + } else if (format === "text") { + result = response.data.body_text; + } + + return result as string; + } catch (e: unknown) { + throw context.logger.error("Fetching issue description failed!", e); + } +} + export async function getAllIssueComments( context: Context, issueNumber: number, format: "raw" | "html" | "text" | "full" = "raw" ): Promise { - const payload = context.event.payload as Payload; - const result: Comment[] = []; - let shouldFetch = true; - let pageNumber = 1; + const payload = context.payload; + + try { + const comments = (await context.octokit.paginate(context.octokit.rest.issues.listComments, { + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issueNumber, + per_page: 100, + mediaType: { + format, + }, + })) as Comment[]; + return comments; + } catch (e: unknown) { + context.logger.error("Fetching all issue comments failed!", e); + return []; + } +} + +export async function getAllIssueAssignEvents(context: Context, issueNumber: number): Promise { + const payload = context.payload; + try { - while (shouldFetch) { - const response = await context.event.octokit.rest.issues.listComments({ + const events = (await context.octokit.paginate( + context.octokit.issues.listEvents, + { owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issueNumber, per_page: 100, - page: pageNumber, - mediaType: { - format, - }, - }); - - await checkRateLimitGit(response?.headers); + }, + (response) => response.data.filter((item) => item.event === "assigned") + )) as AssignEvent[]; - // Fixing infinite loop here, it keeps looping even when its an empty array - if (response?.data?.length > 0) { - response.data.forEach((item) => { - result.push(item as Comment); - }); - pageNumber++; - } else { - shouldFetch = false; - } - } - } catch (e: unknown) { - shouldFetch = false; + return events.sort((a, b) => (new Date(a.created_at) > new Date(b.created_at) ? -1 : 1)); + } catch (err: unknown) { + context.logger.error("Fetching all issue assign events failed!", err); + return []; } - - return result; } export async function checkUserPermissionForRepoAndOrg(context: Context, username: string): Promise { @@ -212,9 +232,9 @@ export async function checkUserPermissionForRepoAndOrg(context: Context, usernam } async function checkUserPermissionForRepo(context: Context, username: string): Promise { - const payload = context.event.payload as Payload; + const payload = context.payload; try { - const res = await context.event.octokit.rest.repos.checkCollaborator({ + const res = await context.octokit.rest.repos.checkCollaborator({ owner: payload.repository.owner.login, repo: payload.repository.name, username, @@ -228,7 +248,7 @@ async function checkUserPermissionForRepo(context: Context, username: string): P } async function checkUserPermissionForOrg(context: Context, username: string): Promise { - const payload = context.event.payload as Payload; + const payload = context.payload; if (!payload.organization) return false; try { @@ -248,7 +268,7 @@ export async function isUserAdminOrBillingManager( context: Context, username: string ): Promise<"admin" | "billing_manager" | false> { - const payload = context.event.payload as Payload; + const payload = context.payload; const isAdmin = await checkIfIsAdmin(); if (isAdmin) return "admin"; @@ -258,7 +278,7 @@ export async function isUserAdminOrBillingManager( return false; async function checkIfIsAdmin() { - const response = await context.event.octokit.rest.repos.getCollaboratorPermissionLevel({ + const response = await context.octokit.rest.repos.getCollaboratorPermissionLevel({ owner: payload.repository.owner.login, repo: payload.repository.name, username, @@ -272,7 +292,7 @@ export async function isUserAdminOrBillingManager( async function checkIfIsBillingManager() { if (!payload.organization) throw context.logger.error(`No organization found in payload!`); - const { data: membership } = await context.event.octokit.rest.orgs.getMembershipForUser({ + const { data: membership } = await context.octokit.rest.orgs.getMembershipForUser({ org: payload.organization.login, username: payload.repository.owner.login, }); @@ -287,126 +307,133 @@ export async function isUserAdminOrBillingManager( } export async function addAssignees(context: Context, issue: number, assignees: string[]) { - const payload = context.event.payload as Payload; + const payload = context.payload; try { - await context.event.octokit.rest.issues.addAssignees({ + await context.octokit.rest.issues.addAssignees({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issue, assignees, }); } catch (e: unknown) { - context.logger.debug("Adding assignees failed!", e); + context.logger.error("Adding assignees failed!", e); } } export async function deleteLabel(context: Context, label: string) { - const payload = context.event.payload as Payload; + const payload = context.payload; try { - const response = await context.event.octokit.rest.search.issuesAndPullRequests({ + const response = await context.octokit.rest.search.issuesAndPullRequests({ q: `repo:${payload.repository.owner.login}/${payload.repository.name} label:"${label}" state:open`, }); if (response.data.items.length === 0) { //remove label - await context.event.octokit.rest.issues.deleteLabel({ + await context.octokit.rest.issues.deleteLabel({ owner: payload.repository.owner.login, repo: payload.repository.name, name: label, }); } } catch (e: unknown) { - context.logger.debug("Deleting label failed!", e); + context.logger.error("Deleting label failed!", e); } } export async function removeLabel(context: Context, name: string) { - const payload = context.event.payload as Payload; + const payload = context.payload; if (!payload.issue) { context.logger.debug("Invalid issue object"); return; } try { - await context.event.octokit.issues.removeLabel({ + await context.octokit.issues.removeLabel({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: payload.issue.number, name: name, }); } catch (e: unknown) { - context.logger.debug("Removing label failed!", e); + context.logger.error("Removing label failed!", e); } } export async function getAllPullRequests(context: Context, state: "open" | "closed" | "all" = "open") { - type Pulls = PromiseType>["data"][0]; - const prArr = [] as Pulls[]; - let isFetchDone = false; - const perPage = 100; - let curPage = 1; - while (!isFetchDone) { - const prs = await getPullRequests(context, state, perPage, curPage); - - // push the objects to array - prArr.push(...prs); + const payload = context.payload; - if (prs.length < perPage) isFetchDone = true; - else curPage++; + try { + const pulls = await context.octokit.paginate(context.octokit.rest.pulls.list, { + owner: payload.repository.owner.login, + repo: payload.repository.name, + state, + per_page: 100, + }); + return pulls; + } catch (err: unknown) { + context.logger.error("Fetching all pull requests failed!", err); + return []; } - return prArr; } -// Use `context.octokit.rest` to get the pull requests for the repository + async function getPullRequests( context: Context, state: "open" | "closed" | "all" = "open", perPage: number, page: number ) { - const payload = context.event.payload as Payload; - const { data: pulls } = await context.event.octokit.rest.pulls.list({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - state, - per_page: perPage, - page, - }); - return pulls; + const payload = context.payload; + + try { + const { data: pulls } = await context.octokit.rest.pulls.list({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + state, + per_page: perPage, + page, + }); + return pulls; + } catch (err: unknown) { + context.logger.error("Fetching pull requests failed!", err); + return []; + } } export async function closePullRequest(context: Context, pullNumber: number) { - const payload = context.event.payload as Payload; + const payload = context.payload as Payload; try { - await context.event.octokit.rest.pulls.update({ + await context.octokit.rest.pulls.update({ owner: payload.repository.owner.login, repo: payload.repository.name, pull_number: pullNumber, state: "closed", }); - } catch (e: unknown) { - context.logger.debug("Closing pull requests failed!", e); + } catch (err: unknown) { + context.logger.error("Closing pull requests failed!", err); } } -async function getAllPullRequestReviews( +export async function getAllPullRequestReviews( context: Context, pullNumber: number, format: "raw" | "html" | "text" | "full" = "raw" ) { - type Reviews = PromiseType>["data"][0]; - const prArr = [] as Reviews[]; - let isFetchDone = false; - const perPage = 100; - let curPage = 1; - while (!isFetchDone) { - const prs = await getPullRequestReviews(context, pullNumber, perPage, curPage, format); + const payload = context.payload; - // push the objects to array - prArr.push(...prs); - - if (prs.length < perPage) isFetchDone = true; - else curPage++; + try { + const reviews = await context.octokit.paginate(context.octokit.rest.pulls.listReviews, { + owner: payload.repository.owner.login, + repo: payload.repository.name, + pull_number: pullNumber, + per_page: 100, + mediaType: { + format, + }, + }); + return reviews; + } catch (err: unknown) { + context.logger.error("Fetching all pull request reviews failed!", err); + return []; } - return prArr; } async function getPullRequestReviews( @@ -416,9 +443,9 @@ async function getPullRequestReviews( page: number, format: "raw" | "html" | "text" | "full" = "raw" ) { - const payload = context.event.payload as Payload; + const payload = context.payload; try { - const { data: reviews } = await context.event.octokit.rest.pulls.listReviews({ + const { data: reviews } = await context.octokit.rest.pulls.listReviews({ owner: payload.repository.owner.login, repo: payload.repository.name, pull_number: pullNumber, @@ -429,63 +456,94 @@ async function getPullRequestReviews( }, }); return reviews; - } catch (e: unknown) { - context.logger.debug("Fetching pull request reviews failed!", e); + } catch (err: unknown) { + context.logger.error("Fetching pull request reviews failed!", err); return []; } } +export async function getReviewRequests(context: Context, pullNumber: number, owner: string, repo: string) { + try { + const response = await context.octokit.pulls.listRequestedReviewers({ + owner: owner, + repo: repo, + pull_number: pullNumber, + }); + return response.data; + } catch (err: unknown) { + context.logger.error("Could not get requested reviewers", err); + return null; + } +} + // Get issues by issue number export async function getIssueByNumber(context: Context, issueNumber: number) { - const payload = context.event.payload as Payload; + const payload = context.payload; try { - const { data: issue } = await context.event.octokit.rest.issues.get({ + const { data: issue } = await context.octokit.rest.issues.get({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issueNumber, }); return issue; } catch (e: unknown) { - context.logger.debug("Fetching issue failed!", e); + context.logger.error("Fetching issue failed!", e); return; } } export async function getPullByNumber(context: Context, pull: number) { - // const runtime = Runtime.getState(); + const payload = context.payload; - const payload = context.event.payload as Payload; - const response = await context.event.octokit.rest.pulls.get({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - pull_number: pull, - }); - return response.data; + try { + const response = await context.octokit.rest.pulls.get({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + pull_number: pull, + }); + return response.data; + } catch (err: unknown) { + context.logger.error("Fetching pull request failed!", err); + return; + } } // Get issues assigned to a username -export async function getAssignedIssues(context: Context, username: string) { - type Issues = PromiseType>["data"][0]; - const issuesArr = [] as Issues[]; - let isFetchDone = false; - const perPage = 100; - let curPage = 1; - while (!isFetchDone) { - const issues = await listIssuesAndPullsForRepo(context, IssueType.OPEN, perPage, curPage); +export async function getAssignedIssues(context: Context, username: string): Promise { + const payload = context.payload; - // push the objects to array - issuesArr.push(...issues); - - if (issues.length < perPage) isFetchDone = true; - else curPage++; + try { + const issues = (await context.octokit.paginate( + context.octokit.issues.listForRepo, + { + owner: payload.repository.owner.login, + repo: payload.repository.name, + state: IssueType.OPEN, + per_page: 1000, + }, + ({ data: issues }) => + issues.filter((issue) => !issue.pull_request && issue.assignee && issue.assignee.login === username) + )) as Issue[]; + return issues; + } catch (err: unknown) { + context.logger.error("Fetching assigned issues failed!", err); + return []; } +} - // get only issues assigned to username - const assignedIssues = issuesArr.filter( - (issue) => !issue.pull_request && issue.assignee && issue.assignee.login === username - ); +export async function getOpenedPullRequestsForAnIssue(context: Context, issueNumber: number, userName: string) { + const pulls = await getOpenedPullRequests(context, userName); - return assignedIssues; + return pulls.filter((pull) => { + if (!pull.body) return false; + const issues = pull.body.match(/#(\d+)/gi); + + if (!issues) return false; + + const linkedIssueNumbers = Array.from(new Set(issues.map((issue) => issue.replace("#", "")))); + if (linkedIssueNumbers.indexOf(`${issueNumber}`) !== -1) return true; + return false; + }); } async function getOpenedPullRequests(context: Context, username: string) { diff --git a/src/types/payload.ts b/src/types/payload.ts index ff1aa2a10..49f1f0ecf 100644 --- a/src/types/payload.ts +++ b/src/types/payload.ts @@ -93,7 +93,7 @@ const userSchema = Type.Object({ export type User = Static; // type UserProfile= Static; -enum AuthorAssociation { +export enum AuthorAssociation { OWNER = "OWNER", COLLABORATOR = "COLLABORATOR", MEMBER = "MEMBER",