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