17a13b901SGabriel Donadel Dall'Agnolimport { Command } from '@expo/commander'; 2226d759eSGabriel Donadel Dall'Agnolimport { User as LinearUser } from '@linear/sdk'; 37a13b901SGabriel Donadel Dall'Agnolimport { UserFilter } from '@linear/sdk/dist/_generated_documents'; 47a13b901SGabriel Donadel Dall'Agnol 57a13b901SGabriel Donadel Dall'Agnolimport * as GitHub from '../GitHub'; 67a13b901SGabriel Donadel Dall'Agnolimport * as Linear from '../Linear'; 77a13b901SGabriel Donadel Dall'Agnolimport logger from '../Logger'; 87a13b901SGabriel Donadel Dall'Agnolimport * as OpenAI from '../OpenAI'; 97a13b901SGabriel Donadel Dall'Agnol 107a13b901SGabriel Donadel Dall'Agnoltype ActionOptions = { 117a13b901SGabriel Donadel Dall'Agnol issue: string; 12226d759eSGabriel Donadel Dall'Agnol importer?: string; 137a13b901SGabriel Donadel Dall'Agnol}; 147a13b901SGabriel Donadel Dall'Agnol 157a13b901SGabriel Donadel Dall'Agnolexport default (program: Command) => { 167a13b901SGabriel Donadel Dall'Agnol program 177a13b901SGabriel Donadel Dall'Agnol .command('import-github-issue-to-linear') 187a13b901SGabriel Donadel Dall'Agnol .alias('igitl') 197a13b901SGabriel Donadel Dall'Agnol .description('Import accepted issues from GitHub to Linear.') 207a13b901SGabriel Donadel Dall'Agnol .option('-i, --issue <string>', 'Number of the issue to import.') 21226d759eSGabriel Donadel Dall'Agnol .option( 22226d759eSGabriel Donadel Dall'Agnol '-imp, --importer [string]', 23226d759eSGabriel Donadel Dall'Agnol '[optional] The username of GitHub account that is importing the issue.' 24226d759eSGabriel Donadel Dall'Agnol ) 257a13b901SGabriel Donadel Dall'Agnol .asyncAction(action); 267a13b901SGabriel Donadel Dall'Agnol}; 277a13b901SGabriel Donadel Dall'Agnol 287a13b901SGabriel Donadel Dall'Agnolasync function action(options: ActionOptions) { 297a13b901SGabriel Donadel Dall'Agnol if (isNaN(Number(options.issue))) { 307a13b901SGabriel Donadel Dall'Agnol throw new Error('Flag `--issue` must be provided with a number value'); 317a13b901SGabriel Donadel Dall'Agnol } 327a13b901SGabriel Donadel Dall'Agnol if (!process.env.GITHUB_TOKEN) { 337a13b901SGabriel Donadel Dall'Agnol throw new Error('Environment variable `GITHUB_TOKEN` is required for this command.'); 347a13b901SGabriel Donadel Dall'Agnol } 357a13b901SGabriel Donadel Dall'Agnol if (!process.env.LINEAR_API_KEY) { 367a13b901SGabriel Donadel Dall'Agnol throw new Error('Environment variable `LINEAR_API_KEY` is required for this command.'); 377a13b901SGabriel Donadel Dall'Agnol } 387a13b901SGabriel Donadel Dall'Agnol 397a13b901SGabriel Donadel Dall'Agnol try { 40226d759eSGabriel Donadel Dall'Agnol await importIssueAsync(+options.issue, options.importer); 417a13b901SGabriel Donadel Dall'Agnol } catch (error) { 427a13b901SGabriel Donadel Dall'Agnol logger.error(error); 437a13b901SGabriel Donadel Dall'Agnol throw error; 447a13b901SGabriel Donadel Dall'Agnol } 457a13b901SGabriel Donadel Dall'Agnol} 467a13b901SGabriel Donadel Dall'Agnol 47226d759eSGabriel Donadel Dall'Agnolasync function importIssueAsync(githubIssueNumber: number, importer?: string) { 487a13b901SGabriel Donadel Dall'Agnol const issue = await GitHub.getIssueAsync(githubIssueNumber); 497a13b901SGabriel Donadel Dall'Agnol if (!issue) { 507a13b901SGabriel Donadel Dall'Agnol throw new Error(`Issue #${githubIssueNumber} does not exist.`); 517a13b901SGabriel Donadel Dall'Agnol } 527a13b901SGabriel Donadel Dall'Agnol 537a13b901SGabriel Donadel Dall'Agnol let issueSummary: string | undefined; 547a13b901SGabriel Donadel Dall'Agnol 557a13b901SGabriel Donadel Dall'Agnol try { 567a13b901SGabriel Donadel Dall'Agnol issueSummary = await OpenAI.askChatGPTAsync( 576ff1ede4SGabriel Donadel Dall'Agnol `Provide a brief summary of the following GitHub issue on the Expo repository in 3 to 5 bullet points, ignoring the environment section. Keep in mind that this is the user's perspective, and any judgement they share around priority may not match the opinion of maintainers. This summary will be read by the Expo project maintainers.\n${issue.body}` 587a13b901SGabriel Donadel Dall'Agnol ); 597a13b901SGabriel Donadel Dall'Agnol } catch (error) { 607a13b901SGabriel Donadel Dall'Agnol logger.warn('Failed to generate issue summary using OpenAI. Skipping...'); 617a13b901SGabriel Donadel Dall'Agnol logger.debug(`OpenAI askChatGPTAsync error: ${error}`); 627a13b901SGabriel Donadel Dall'Agnol } 637a13b901SGabriel Donadel Dall'Agnol 64226d759eSGabriel Donadel Dall'Agnol let issueDescription = `### This issue was automatically imported from GitHub: ${issue.html_url}\n`; 65226d759eSGabriel Donadel Dall'Agnol 66226d759eSGabriel Donadel Dall'Agnol let importerLinearUser: LinearUser | undefined; 67226d759eSGabriel Donadel Dall'Agnol if (importer && (importerLinearUser = await inferLinearUserId([importer]))) { 68226d759eSGabriel Donadel Dall'Agnol issueDescription += `#### Issue accepted by @${importerLinearUser.displayName}\n`; 69226d759eSGabriel Donadel Dall'Agnol } 70*7195f9f8SGabriel Donadel Dall'Agnol if (issueSummary) { 71226d759eSGabriel Donadel Dall'Agnol issueDescription += `---\n## Summary:\n${issueSummary}`; 72*7195f9f8SGabriel Donadel Dall'Agnol } 737a13b901SGabriel Donadel Dall'Agnol 747a13b901SGabriel Donadel Dall'Agnol const githubLabel = await Linear.getOrCreateLabelAsync('GitHub'); 757a13b901SGabriel Donadel Dall'Agnol const expoSDKLabel = await Linear.getOrCreateLabelAsync('Expo SDK', Linear.ENG_TEAM_ID); 767a13b901SGabriel Donadel Dall'Agnol const backlogWorkflowState = await Linear.getTeamWorkflowStateAsync( 777a13b901SGabriel Donadel Dall'Agnol 'Backlog', 787a13b901SGabriel Donadel Dall'Agnol Linear.ENG_TEAM_ID 797a13b901SGabriel Donadel Dall'Agnol ); 807a13b901SGabriel Donadel Dall'Agnol 817a13b901SGabriel Donadel Dall'Agnol Linear.createIssueAsync({ 827a13b901SGabriel Donadel Dall'Agnol title: issue.title, 837a13b901SGabriel Donadel Dall'Agnol labelIds: [githubLabel.id, expoSDKLabel.id], 847a13b901SGabriel Donadel Dall'Agnol stateId: backlogWorkflowState.id, 857a13b901SGabriel Donadel Dall'Agnol description: issueDescription, 86226d759eSGabriel Donadel Dall'Agnol assigneeId: (await inferLinearUserId(issue.assignees?.map(({ login }) => login)))?.id, 87226d759eSGabriel Donadel Dall'Agnol subscriberIds: importerLinearUser?.id ? [importerLinearUser.id] : undefined, 887a13b901SGabriel Donadel Dall'Agnol }); 897a13b901SGabriel Donadel Dall'Agnol} 907a13b901SGabriel Donadel Dall'Agnol 91226d759eSGabriel Donadel Dall'Agnolasync function inferLinearUserId(githubUsernames?: string[]): Promise<LinearUser | undefined> { 92226d759eSGabriel Donadel Dall'Agnol if (!githubUsernames?.length) { 937a13b901SGabriel Donadel Dall'Agnol return undefined; 947a13b901SGabriel Donadel Dall'Agnol } 957a13b901SGabriel Donadel Dall'Agnol 967a13b901SGabriel Donadel Dall'Agnol const githubUsers = await Promise.all( 97226d759eSGabriel Donadel Dall'Agnol githubUsernames.map(async (u) => await GitHub.getUserAsync(u)) 987a13b901SGabriel Donadel Dall'Agnol ); 997a13b901SGabriel Donadel Dall'Agnol 1007a13b901SGabriel Donadel Dall'Agnol const linearUsers = await Linear.getTeamMembersAsync({ 1017a13b901SGabriel Donadel Dall'Agnol teamId: Linear.ENG_TEAM_ID, 1027a13b901SGabriel Donadel Dall'Agnol filter: { 1037a13b901SGabriel Donadel Dall'Agnol or: githubUsers.reduce((acc: UserFilter[], cur) => { 1047a13b901SGabriel Donadel Dall'Agnol acc.push({ displayName: { eqIgnoreCase: cur.login } }); 1057a13b901SGabriel Donadel Dall'Agnol if (cur.name) { 1067a13b901SGabriel Donadel Dall'Agnol acc.push({ name: { containsIgnoreCase: cur.name } }); 1077a13b901SGabriel Donadel Dall'Agnol } 1087a13b901SGabriel Donadel Dall'Agnol if (cur.email) { 1097a13b901SGabriel Donadel Dall'Agnol acc.push({ email: { eq: cur.email } }); 1107a13b901SGabriel Donadel Dall'Agnol } 1117a13b901SGabriel Donadel Dall'Agnol 1127a13b901SGabriel Donadel Dall'Agnol return acc; 1137a13b901SGabriel Donadel Dall'Agnol }, []), 1147a13b901SGabriel Donadel Dall'Agnol }, 1157a13b901SGabriel Donadel Dall'Agnol }); 1167a13b901SGabriel Donadel Dall'Agnol 117226d759eSGabriel Donadel Dall'Agnol return linearUsers?.[0]; 1187a13b901SGabriel Donadel Dall'Agnol} 119