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