xref: /expo/tools/src/GitHubActions.ts (revision 1e029c89)
1import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
2import fs from 'fs-extra';
3import path from 'path';
4
5import { EXPO_DIR } from './Constants';
6import { getPullRequestAsync } from './GitHub';
7import { execAll, filterAsync } from './Utils';
8
9const octokit = new Octokit({
10  auth: process.env.GITHUB_TOKEN,
11});
12
13// Predefine some params used across almost all requests.
14const owner = 'expo';
15const repo = 'expo';
16
17export type Workflow =
18  RestEndpointMethodTypes['actions']['listRepoWorkflows']['response']['data']['workflows'][0] & {
19    slug: string;
20    baseSlug: string;
21    inputs?: Record<string, string>;
22  };
23
24export type WorkflowDispatchEventInputs = Record<string, string>;
25
26/**
27 * Requests for the list of active workflows.
28 */
29export async function getWorkflowsAsync(): Promise<Workflow[]> {
30  const response = await octokit.actions.listRepoWorkflows({
31    owner,
32    repo,
33  });
34
35  // We need to filter out some workflows because they might have
36  // - empty `name` or `path` (why?)
37  // - inactive state
38  // - workflow config no longer exists
39  const workflows = await filterAsync(response.data.workflows, async (workflow) =>
40    Boolean(
41      workflow.name &&
42        workflow.path &&
43        workflow.state === 'active' &&
44        (await fs.pathExists(path.join(EXPO_DIR, workflow.path)))
45    )
46  );
47  return workflows
48    .sort((a, b) => a.name.localeCompare(b.name))
49    .map((workflow) => {
50      const slug = path.basename(workflow.path, path.extname(workflow.path));
51      return {
52        ...workflow,
53        slug,
54        baseSlug: slug,
55      };
56    });
57}
58
59/**
60 * Requests for the list of manually triggered runs for given workflow ID.
61 */
62export async function getWorkflowRunsAsync(workflow_id: number, event?: string) {
63  const { data } = await octokit.actions.listWorkflowRuns({
64    owner,
65    repo,
66    workflow_id,
67    event,
68  });
69  return data.workflow_runs;
70}
71
72/**
73 * Resolves to the recently dispatched workflow run.
74 */
75export async function getLatestDispatchedWorkflowRunAsync(workflowId: number) {
76  const workflowRuns = await getWorkflowRunsAsync(workflowId, 'workflow_dispatch');
77  return workflowRuns[0] ?? null;
78}
79
80/**
81 * Requests for the list of job for workflow run with given ID.
82 */
83export async function getJobsForWorkflowRunAsync(run_id: number) {
84  const { data } = await octokit.actions.listJobsForWorkflowRun({
85    owner,
86    repo,
87    run_id,
88  });
89  return data.jobs;
90}
91
92/**
93 * Dispatches an event that triggers a workflow with given ID or workflow filename (including extension).
94 */
95export async function dispatchWorkflowEventAsync(
96  workflow_id: number | string,
97  ref: string,
98  inputs?: WorkflowDispatchEventInputs
99): Promise<void> {
100  await octokit.actions.createWorkflowDispatch({
101    owner,
102    repo,
103    workflow_id,
104    ref,
105    inputs: inputs ?? {},
106  });
107}
108
109/**
110 * Returns an array of issue IDs that has been auto-closed by the pull request.
111 */
112export async function getClosedIssuesAsync(pullRequestId: number): Promise<number[]> {
113  const pullRequest = await getPullRequestAsync(pullRequestId, true);
114  const matches = execAll(
115    /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved) (#|https:\/\/github\.com\/expo\/expo\/issues\/)(\d+)/gi,
116    pullRequest.body ?? '',
117    2
118  );
119  return matches.map((match) => parseInt(match, 10)).filter((issue) => !isNaN(issue));
120}
121
122/**
123 * Creates an issue comment with given body.
124 */
125export async function commentOnIssueAsync(issue_number: number, body: string) {
126  const { data } = await octokit.issues.createComment({
127    owner,
128    repo,
129    issue_number,
130    body,
131  });
132  return data;
133}
134