1import chalk from 'chalk';
2import inquirer from 'inquirer';
3
4import { UNPUBLISHED_VERSION_NAME } from '../../Changelogs';
5import { link } from '../../Formatter';
6import * as GitHub from '../../GitHub';
7import logger from '../../Logger';
8import { Task } from '../../TasksRunner';
9import { runWithSpinner } from '../../Utils';
10import { Parcel, TaskArgs } from '../types';
11import { selectPackagesToPublish } from './selectPackagesToPublish';
12
13// https://github.com/expo/expo/pulls?q=label:published
14const PUBLISHED_LABEL_NAME = 'published';
15
16const { green, blue, magenta, bold } = chalk;
17
18/**
19 * Adds "published" label to pull requests mentioned in changelog entries.
20 */
21export const addPublishedLabelToPullRequests = new Task<TaskArgs>(
22  {
23    name: 'addPublishedLabelToPullRequests',
24    dependsOn: [selectPackagesToPublish],
25  },
26  async (parcels: Parcel[]) => {
27    if (!process.env.GITHUB_TOKEN) {
28      logger.error(
29        'Environment variable `%s` must be set to add labels to pull requests',
30        magenta('GITHUB_TOKEN')
31      );
32      return;
33    }
34
35    // A set of pull request IDs extracted from changelog entries
36    const pullRequestIds = new Set<number>();
37
38    // Find all pull requests mentioned in changelogs
39    for (const { changelogChanges } of parcels) {
40      const versionChanges = changelogChanges.versions[UNPUBLISHED_VERSION_NAME];
41
42      if (!versionChanges) {
43        continue;
44      }
45      for (const entry of Object.values(versionChanges).flat()) {
46        entry.pullRequests?.forEach((pullRequestId) => {
47          pullRequestIds.add(pullRequestId);
48        });
49      }
50    }
51
52    if (pullRequestIds.size === 0) {
53      return;
54    }
55
56    // Request for pull request objects for the extracted IDs
57    // This needs to happen consecutively to reduce the risk of being rate-limited by GitHub
58    const pullRequests = await runWithSpinner(
59      'Requesting published pull requests from GitHub',
60      async () => {
61        const pullRequests: GitHub.PullRequest[] = [];
62
63        for (const pullRequestId of pullRequestIds) {
64          const pullRequest = await GitHub.getPullRequestAsync(pullRequestId, true);
65          const hasLabel = pullRequest.labels.some((label) => label.name === PUBLISHED_LABEL_NAME);
66
67          if (!hasLabel) {
68            pullRequests.push(pullRequest);
69          }
70        }
71        return pullRequests;
72      },
73      'Loaded published pull requests from GitHub'
74    );
75
76    // Skip the rest if all pull requests already have the label
77    if (pullRequests.length === 0) {
78      logger.log('There are no pull requests that are not labeled already');
79      return;
80    }
81
82    // Select pull requests to mark as published
83    const pullRequestsToLabel = await selectPullRequestsToLabel(pullRequests);
84
85    // Finally, consecutively add the label to each pull request
86    await runWithSpinner(
87      'Adding the label to selected pull requests',
88      async () => {
89        for (const pullRequest of pullRequestsToLabel) {
90          await GitHub.addIssueLabelsAsync(pullRequest.number, [PUBLISHED_LABEL_NAME]);
91        }
92      },
93      'Added the published label'
94    );
95
96    logger.log();
97  }
98);
99
100function linkToPullRequest(pr: GitHub.PullRequest): string {
101  return link(blue('#' + pr.number), pr.html_url);
102}
103
104function linkToAuthor(pr: GitHub.PullRequest): string {
105  const { user } = pr;
106  return user ? link(green('@' + user.login), user.html_url) : 'anonymous';
107}
108
109function formatPullRequest(pr: GitHub.PullRequest): string {
110  return `${linkToPullRequest(pr)}: ${bold(pr.title)} (by ${linkToAuthor(pr)})`;
111}
112
113/**
114 * Prompts the user to select pull requests that should be labeled as published.
115 */
116async function selectPullRequestsToLabel(
117  pullRequests: GitHub.PullRequest[]
118): Promise<GitHub.PullRequest[]> {
119  const { selectedPullRequests } = await inquirer.prompt([
120    {
121      type: 'checkbox',
122      name: 'selectedPullRequests',
123      message: 'Which pull requests do you want to label as published?\n',
124      choices: pullRequests.map((pr) => {
125        return {
126          name: formatPullRequest(pr),
127          value: pr,
128          checked: true,
129        };
130      }),
131    },
132  ]);
133  return selectedPullRequests as GitHub.PullRequest[];
134}
135