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