import { Octokit, RestEndpointMethodTypes } from '@octokit/rest'; import parseDiff from 'parse-diff'; import path from 'path'; import { EXPO_DIR } from './Constants'; import { GitFileDiff } from './Git'; const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN, }); const cachedPullRequests = new Map(); // Predefine some params used across almost all requests. const owner = 'expo'; const repo = 'expo'; /** * Returns public informations about the currently authenticated (by GitHub API token) user. */ export async function getAuthenticatedUserAsync() { const { data } = await octokit.users.getAuthenticated(); return data; } /** * Returns public user data by the given username. * @param username - The username of the user to retrieve. */ export async function getUserAsync(username: string) { const { data } = await octokit.users.getByUsername({ username }); return data; } /** * Requests for the pull request object. */ export async function getPullRequestAsync( pull_number: number, cached: boolean = false ): Promise { if (cached) { const cachedPullRequest = cachedPullRequests.get(pull_number); if (cachedPullRequest) { return cachedPullRequest; } } const { data } = await octokit.pulls.get({ owner, repo, pull_number, }); cachedPullRequests.set(pull_number, data); return data; } /** * Returns the url of the PR that closed an issue. */ export async function getIssueCloserPrUrlAsync(issueNumber: number): Promise { const { repository } = await octokit.graphql( `query GetIssueCloser($repo: String!, $owner: String!, $issueNumber: Int!) { repository(name: $repo, owner: $owner) { issue(number: $issueNumber) { timelineItems(itemTypes: CLOSED_EVENT, last: 1) { nodes { ... on ClosedEvent { createdAt closer { __typename ... on PullRequest { url } } } } } } } }`, { owner, repo, issueNumber, } ); return repository?.issue?.timelineItems?.nodes?.[0]?.closer?.url; } /** * Requests and parses the diff of the pull request with given number. */ export async function getPullRequestDiffAsync( pull_number: number, base_path: string = EXPO_DIR ): Promise { const { data } = await octokit.pulls.get({ owner, repo, pull_number, headers: { accept: 'application/vnd.github.v3.diff', }, }); // When the custom accept header is provided the returned data // doesn't match declared type (it's a string). const diff = parseDiff(data as unknown as string); return diff.map((entry) => { return { ...entry, path: path.join(base_path, (entry.deleted ? entry.from : entry.to)!), }; }); } /** * Gets a list of reviews left in the pull request with given ID. */ export async function listPullRequestReviewsAsync( pull_number: number ): Promise { const { data } = await octokit.pulls.listReviews({ owner, repo, pull_number, }); return data; } /** * Creates pull request review. By default the review is pending which needs to be submitted in order to be visible for other users. * Provide `event` option to create and submit at once. */ export async function createPullRequestReviewAsync( pull_number: number, options?: T ): Promise { const { data } = await octokit.pulls.createReview({ owner, repo, pull_number, ...options, }); return data; } /** * Updates pull request review with a new main comment. */ export async function updatePullRequestReviewAsync( pull_number: number, review_id: number, body: string ) { const { data } = await octokit.pulls.updateReview({ owner, repo, pull_number, review_id, body, }); return data; } /** * Gets a list of comments in review. */ export async function listPullRequestReviewCommentsAsync(pull_number: number, review_id: number) { const { data } = await octokit.pulls.listReviewComments({ owner, repo, pull_number, review_id, }); return data; } /** * Deletes a comment left under pull request review. */ export async function deletePullRequestReviewCommentAsync(comment_id: number) { const { data } = await octokit.pulls.deleteReviewComment({ owner, repo, comment_id, }); return data; } /** * Deletes all comments from given review. */ export async function deleteAllPullRequestReviewCommentsAsync( pull_number: number, review_id: number ) { const comments = await listPullRequestReviewCommentsAsync(pull_number, review_id); await Promise.all( comments .filter((comment) => comment.pull_request_review_id === review_id) .map((comment) => deletePullRequestReviewCommentAsync(comment.id)) ); } /** * Requests given users to review the pull request. * If the user already reviewed the PR, it resets his review state. */ export async function requestPullRequestReviewersAsync(pull_number: number, reviewers: string[]) { const { data } = await octokit.pulls.requestReviewers({ owner, repo, pull_number, reviewers, }); return data; } /** * Returns an issue object with given issue number. */ export async function getIssueAsync(issue_number: number) { const { data } = await octokit.issues.get({ owner, repo, issue_number, }); return data; } /** * Returns a list of all open issues. Limited to 10 items. */ export async function listAllOpenIssuesAsync({ limit, offset, labels, }: { limit?: number; offset?: number; labels?: string; } = {}) { const per_page = limit ?? 10; const page = offset ? offset * per_page : 0; const { data } = await octokit.issues.listForRepo({ owner, repo, per_page, labels, page, state: 'open', }); return data; } /** * Creates an issue comment with given body. */ export async function createCommentAsync(issue_number: number, body: string) { const { data } = await octokit.issues.createComment({ owner, repo, issue_number, body, }); return data; } /** * Lists commits in given issue. */ export async function listCommentsAsync( issue_number: number, options: Partial ) { const { data } = await octokit.issues.listComments({ owner, repo, issue_number, ...options, }); return data; } /** * Returns a list of issue comments gathered from all pages. */ export async function listAllCommentsAsync(issue_number: number) { const issue = await getIssueAsync(issue_number); const comments = [] as ListCommentsResponse['data']; const pageSize = 100; for (let page = 1, maxPage = Math.ceil(issue.comments / pageSize); page <= maxPage; page++) { const commentsPage = await listCommentsAsync(issue_number, { page, per_page: pageSize, }); comments.push(...commentsPage); } return comments; } /** * Deletes an issue comment with given identifier. */ export async function deleteCommentAsync(comment_id: number) { const { data } = await octokit.issues.deleteComment({ owner, repo, comment_id, }); return data; } /** * Adds labels to the issue. Throws an error when any of given labels doesn't exist. */ export async function addIssueLabelsAsync(issue_number: number, labels: string[]) { const { data } = await octokit.issues.addLabels({ owner, repo, issue_number, labels, }); return data; } /** * Removes single label from the issue. * Throws an error when given label doesn't exist and when the label isn't added. */ export async function removeIssueLabelAsync(issue_number: number, name: string) { const { data } = await octokit.issues.removeLabel({ owner, repo, issue_number, name, }); return data; } // Octokit's types are autogenerated and so inconvenient to use if you want to refer to them. // We re-export some of them to make it easier. export type PullRequestReviewEvent = 'COMMENT' | 'APPROVE' | 'REQUEST_CHANGES'; export type PullRequest = RestEndpointMethodTypes['pulls']['get']['response']['data']; export type PullRequestReview = RestEndpointMethodTypes['pulls']['getReview']['response']['data']; export type IssueComment = RestEndpointMethodTypes['issues']['getComment']['response']['data']; export type ListCommentsOptions = RestEndpointMethodTypes['issues']['listComments']['parameters']; export type ListCommentsResponse = RestEndpointMethodTypes['issues']['listComments']['response'];