xref: /expo/tools/src/GitHub.ts (revision f965d028)
1import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
2import parseDiff from 'parse-diff';
3import path from 'path';
4
5import { EXPO_DIR } from './Constants';
6import { GitFileDiff } from './Git';
7
8const octokit = new Octokit({
9  auth: process.env.GITHUB_TOKEN,
10});
11
12// Predefine some params used across almost all requests.
13const owner = 'expo';
14const repo = 'expo';
15
16/**
17 * Returns public informations about the currently authenticated (by GitHub API token) user.
18 */
19export async function getAuthenticatedUserAsync() {
20  const { data } = await octokit.users.getAuthenticated();
21  return data;
22}
23
24/**
25 * Requests for the pull request object.
26 */
27export async function getPullRequestAsync(pull_number: number): Promise<PullRequest> {
28  const { data } = await octokit.pulls.get({
29    owner,
30    repo,
31    pull_number,
32  });
33  return data;
34}
35
36/**
37 * Requests and parses the diff of the pull request with given number.
38 */
39export async function getPullRequestDiffAsync(
40  pull_number: number,
41  base_path: string = EXPO_DIR
42): Promise<GitFileDiff[]> {
43  const { data } = await octokit.pulls.get({
44    owner,
45    repo,
46    pull_number,
47    headers: {
48      accept: 'application/vnd.github.v3.diff',
49    },
50  });
51
52  // When the custom accept header is provided the returned data
53  // doesn't match declared type (it's a string).
54  const diff = parseDiff(data as unknown as string);
55
56  return diff.map((entry) => {
57    return {
58      ...entry,
59      path: path.join(base_path, (entry.deleted ? entry.from : entry.to)!),
60    };
61  });
62}
63
64/**
65 * Gets a list of reviews left in the pull request with given ID.
66 */
67export async function listPullRequestReviewsAsync(
68  pull_number: number
69): Promise<PullRequestReview[]> {
70  const { data } = await octokit.pulls.listReviews({
71    owner,
72    repo,
73    pull_number,
74  });
75  return data;
76}
77
78/**
79 * Creates pull request review. By default the review is pending which needs to be submitted in order to be visible for other users.
80 * Provide `event` option to create and submit at once.
81 */
82export async function createPullRequestReviewAsync<T>(
83  pull_number: number,
84  options?: T
85): Promise<PullRequestReview> {
86  const { data } = await octokit.pulls.createReview({
87    owner,
88    repo,
89    pull_number,
90    ...options,
91  });
92  return data;
93}
94
95/**
96 * Updates pull request review with a new main comment.
97 */
98export async function updatePullRequestReviewAsync(
99  pull_number: number,
100  review_id: number,
101  body: string
102) {
103  const { data } = await octokit.pulls.updateReview({
104    owner,
105    repo,
106    pull_number,
107    review_id,
108    body,
109  });
110  return data;
111}
112
113/**
114 * Gets a list of comments in review.
115 */
116export async function listPullRequestReviewCommentsAsync(pull_number: number, review_id: number) {
117  const { data } = await octokit.pulls.listReviewComments({
118    owner,
119    repo,
120    pull_number,
121    review_id,
122  });
123  return data;
124}
125
126/**
127 * Deletes a comment left under pull request review.
128 */
129export async function deletePullRequestReviewCommentAsync(comment_id: number) {
130  const { data } = await octokit.pulls.deleteReviewComment({
131    owner,
132    repo,
133    comment_id,
134  });
135  return data;
136}
137
138/**
139 * Deletes all comments from given review.
140 */
141export async function deleteAllPullRequestReviewCommentsAsync(
142  pull_number: number,
143  review_id: number
144) {
145  const comments = await listPullRequestReviewCommentsAsync(pull_number, review_id);
146
147  await Promise.all(
148    comments
149      .filter((comment) => comment.pull_request_review_id === review_id)
150      .map((comment) => deletePullRequestReviewCommentAsync(comment.id))
151  );
152}
153
154/**
155 * Requests given users to review the pull request.
156 * If the user already reviewed the PR, it resets his review state.
157 */
158export async function requestPullRequestReviewersAsync(pull_number: number, reviewers: string[]) {
159  const { data } = await octokit.pulls.requestReviewers({
160    owner,
161    repo,
162    pull_number,
163    reviewers,
164  });
165  return data;
166}
167
168/**
169 * Returns an issue object with given issue number.
170 */
171export async function getIssueAsync(issue_number: number) {
172  const { data } = await octokit.issues.get({
173    owner,
174    repo,
175    issue_number,
176  });
177  return data;
178}
179
180/**
181 * Creates an issue comment with given body.
182 */
183export async function createCommentAsync(issue_number: number, body: string) {
184  const { data } = await octokit.issues.createComment({
185    owner,
186    repo,
187    issue_number,
188    body,
189  });
190  return data;
191}
192
193/**
194 * Lists commits in given issue.
195 */
196export async function listCommentsAsync(
197  issue_number: number,
198  options: Partial<ListCommentsOptions>
199) {
200  const { data } = await octokit.issues.listComments({
201    owner,
202    repo,
203    issue_number,
204    ...options,
205  });
206  return data;
207}
208
209/**
210 * Returns a list of issue comments gathered from all pages.
211 */
212export async function listAllCommentsAsync(issue_number: number) {
213  const issue = await getIssueAsync(issue_number);
214  const comments = [] as ListCommentsResponse['data'];
215  const pageSize = 100;
216
217  for (let page = 1, maxPage = Math.ceil(issue.comments / pageSize); page <= maxPage; page++) {
218    const commentsPage = await listCommentsAsync(issue_number, {
219      page,
220      per_page: pageSize,
221    });
222    comments.push(...commentsPage);
223  }
224  return comments;
225}
226
227/**
228 * Deletes an issue comment with given identifier.
229 */
230export async function deleteCommentAsync(comment_id: number) {
231  const { data } = await octokit.issues.deleteComment({
232    owner,
233    repo,
234    comment_id,
235  });
236  return data;
237}
238
239/**
240 * Adds labels to the issue. Throws an error when any of given labels doesn't exist.
241 */
242export async function addIssueLabelsAsync(issue_number: number, labels: string[]) {
243  const { data } = await octokit.issues.addLabels({
244    owner,
245    repo,
246    issue_number,
247    labels,
248  });
249  return data;
250}
251
252/**
253 * Removes single label from the issue.
254 * Throws an error when given label doesn't exist and when the label isn't added.
255 */
256export async function removeIssueLabelAsync(issue_number: number, name: string) {
257  const { data } = await octokit.issues.removeLabel({
258    owner,
259    repo,
260    issue_number,
261    name,
262  });
263  return data;
264}
265
266// Octokit's types are autogenerated and so inconvenient to use if you want to refer to them.
267// We re-export some of them to make it easier.
268export type PullRequestReviewEvent = 'COMMENT' | 'APPROVE' | 'REQUEST_CHANGES';
269export type PullRequest = RestEndpointMethodTypes['pulls']['get']['response']['data'];
270export type PullRequestReview = RestEndpointMethodTypes['pulls']['getReview']['response']['data'];
271export type IssueComment = RestEndpointMethodTypes['issues']['getComment']['response']['data'];
272export type ListCommentsOptions = RestEndpointMethodTypes['issues']['listComments']['parameters'];
273export type ListCommentsResponse = RestEndpointMethodTypes['issues']['listComments']['response'];
274