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