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