diff --git a/app.yml b/app.yml index 557e6c2cd..1ac1d99dd 100644 --- a/app.yml +++ b/app.yml @@ -24,18 +24,18 @@ default_events: # - gollum - issue_comment - issues -# - label -# - milestone -# - member -# - membership -# - org_block -# - organization -# - page_build -# - project -# - project_card -# - project_column -# - public -# - pull_request + # - label + # - milestone + # - member + # - membership + # - org_block + # - organization + # - page_build + # - project + # - project_card + # - project_column + # - public + - pull_request # - pull_request_review # - pull_request_review_comment # - push @@ -82,7 +82,7 @@ default_permissions: # Pull requests and related comments, assignees, labels, milestones, and merges. # https://developer.github.com/v3/apps/permissions/#permission-on-pull-requests - # pull_requests: read + pull_requests: read # Manage the post-receive hooks for a repository. # https://developer.github.com/v3/apps/permissions/#permission-on-repository-hooks diff --git a/src/handlers/assign/auto.ts b/src/handlers/assign/auto.ts new file mode 100644 index 000000000..d46a46aa1 --- /dev/null +++ b/src/handlers/assign/auto.ts @@ -0,0 +1,51 @@ +import { getBotContext, getLogger } from "../../bindings"; +import { addAssignees, getIssueByNumber, getPullRequests } from "../../helpers"; +import { gitLinkedIssueParser } from "../../helpers/parser"; +import { Payload } from "../../types"; + +// Check for pull requests linked to their respective issues but not assigned to them +export const checkPullRequests = async () => { + const context = getBotContext(); + const logger = getLogger(); + const pulls = await getPullRequests(context); + + if (pulls.length === 0) { + logger.debug(`No pull requests found at this time`); + return; + } + + const payload = context.payload as Payload; + + // Loop through the pull requests and assign them to their respective issues if needed + for (const pull of pulls) { + let pullRequestLinked = await gitLinkedIssueParser({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: pull.number, + }); + + // if pullRequestLinked is empty, continue + if (pullRequestLinked == "") { + continue; + } + + const linkedIssueNumber = pullRequestLinked.substring(pullRequestLinked.lastIndexOf("/") + 1); + + // Check if the pull request opener is assigned to the issue + const opener = pull!.user!.login; + + let issue = await getIssueByNumber(context, +linkedIssueNumber); + + // if issue is already assigned, continue + if (issue!.assignees!.length > 0) { + logger.debug(`Issue already assigned, ignoring...`); + continue; + } + + const assignedUsernames = issue!.assignees!.map((assignee) => assignee.login); + if (!assignedUsernames.includes(opener)) { + await addAssignees(+linkedIssueNumber, [opener]); + logger.debug(`Assigned pull request #${pull.number} opener to issue ${linkedIssueNumber}.`); + } + } +}; diff --git a/src/handlers/processors.ts b/src/handlers/processors.ts index 5021ab54e..151e28ceb 100644 --- a/src/handlers/processors.ts +++ b/src/handlers/processors.ts @@ -5,6 +5,7 @@ import { checkBountiesToUnassign, collectAnalytics, checkWeeklyUpdate } from "./ import { nullHandler } from "./shared"; import { handleComment } from "./comment"; import { handleIssueClosed } from "./payout"; +import { checkPullRequests } from "./assign/auto"; export const processors: Record = { [GithubEvent.ISSUES_LABELED]: { @@ -37,6 +38,11 @@ export const processors: Record = { action: [handleIssueClosed], post: [nullHandler], }, + [GithubEvent.PULL_REQUEST_OPENED]: { + pre: [nullHandler], + action: [checkPullRequests], + post: [nullHandler], + }, }; /** diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts index 74f16b17f..d60df9ee7 100644 --- a/src/helpers/issue.ts +++ b/src/helpers/issue.ts @@ -1,3 +1,4 @@ +import { Context } from "probot"; import { getBotContext, getLogger } from "../bindings"; import { Comment, Payload } from "../types"; import { checkRateLimitGit } from "../utils"; @@ -187,3 +188,37 @@ export const deleteLabel = async (label: string): Promise => { logger.debug(`Label deletion failed!, reason: ${e}`); } }; + +// Use `context.octokit.rest` to get the pull requests for the repository +export const getPullRequests = async (context: Context, state: "open" | "closed" | "all" = "open") => { + const logger = getLogger(); + const payload = context.payload as Payload; + try { + const { data: pulls } = await context.octokit.rest.pulls.list({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + state, + }); + return pulls; + } catch (e: unknown) { + logger.debug(`Fetching pull requests failed!, reason: ${e}`); + return []; + } +}; + +// Get issues by issue number +export const getIssueByNumber = async (context: Context, issue_number: number) => { + const logger = getLogger(); + const payload = context.payload as Payload; + try { + const { data: issue } = await context.octokit.rest.issues.get({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number, + }); + return issue; + } catch (e: unknown) { + logger.debug(`Fetching issue failed!, reason: ${e}`); + return; + } +}; diff --git a/src/helpers/parser.ts b/src/helpers/parser.ts index 517c7cbe9..b05411fee 100644 --- a/src/helpers/parser.ts +++ b/src/helpers/parser.ts @@ -24,3 +24,21 @@ export const gitIssueParser = async ({ owner, repo, issue_number }: GitParser): return true; } }; + +export const gitLinkedIssueParser = async ({ owner, repo, issue_number }: GitParser): Promise => { + try { + const { data } = await axios.get(`https://github.com/${owner}/${repo}/pull/${issue_number}`); + const dom = parse(data); + const devForm = dom.querySelector("[data-target='create-branch.developmentForm']") as HTMLElement; + const linkedPRs = devForm.querySelectorAll(".my-1"); + + if (linkedPRs.length === 0) { + return ""; + } + + const prUrl = linkedPRs[0].querySelector("a")?.attrs?.href || ""; + return prUrl; + } catch (error) { + return ""; + } +}; diff --git a/src/types/payload.ts b/src/types/payload.ts index dea72faf0..9418c29ac 100644 --- a/src/types/payload.ts +++ b/src/types/payload.ts @@ -13,6 +13,9 @@ export enum GithubEvent { // issue_comment ISSUE_COMMENT_CREATED = "issue_comment.created", ISSUE_COMMENT_EDITED = "issue_comment.edited", + + // pull_request + PULL_REQUEST_OPENED = "pull_request.opened", } export enum UserType {