1import { Command } from '@expo/commander';
2
3import { getIssueAsync, addIssueLabelsAsync } from '../GitHub';
4import logger from '../Logger';
5
6type ActionOptions = {
7  issue: string;
8};
9
10export default (program: Command) => {
11  program
12    .command('validate-issue')
13    .alias('vi')
14    .description('Verifies whether a GitHub issue is valid.')
15    .option('-i, --issue <string>', 'Number of the issue to validate.')
16    .asyncAction(action);
17};
18
19async function action(options: ActionOptions) {
20  if (isNaN(Number(options.issue))) {
21    throw new Error('Flag `--issue` must be provided with a number value.');
22  }
23  if (!process.env.GITHUB_TOKEN) {
24    throw new Error('Environment variable `GITHUB_TOKEN` is required for this command.');
25  }
26  try {
27    await validateIssueAsync(+options.issue);
28  } catch (error) {
29    logger.error(error);
30    throw error;
31  }
32}
33
34const REPRO_URI_REGEXES = [
35  /github\.com/,
36  /gitlab\.com/,
37  /bitbucket\.org/,
38  /snack\.expo\.(dev|io)\//,
39];
40
41const SKIP_VALIDATION_LABELS = [
42  '!',
43  'Issue accepted',
44  'invalid issue: feature request',
45  'CLI',
46  'invalid issue: question',
47  'docs',
48];
49
50async function validateIssueAsync(issueNumber: number) {
51  const issue = await getIssueAsync(issueNumber);
52  if (!issue) {
53    throw new Error(`Issue #${issueNumber} does not exist.`);
54  }
55
56  // Skip if we already applied some other label
57  for (const label of issue.labels) {
58    const labelName = typeof label === 'string' ? label : label.name;
59    if (labelName && SKIP_VALIDATION_LABELS.includes(labelName)) {
60      console.log(`Issue is labeled with ${labelName}, skipping validation.`);
61      return;
62    }
63  }
64
65  // Maybe actually match the full URL and print it?
66  const matches = REPRO_URI_REGEXES.map((regex) =>
67    (issue.body?.toLowerCase() ?? '').match(regex)
68  ).filter(Boolean);
69
70  const includesReproUri = matches.length > 0;
71
72  if (includesReproUri) {
73    console.log('Issue includes a reprodible example URI.');
74  } else {
75    addIssueLabelsAsync(issueNumber, ['incomplete issue: missing or invalid repro']);
76    console.log('No reproducible example provided, marked for closing.');
77  }
78}
79