/ src / github-api-helper.ts
github-api-helper.ts
  1  import * as assert from 'assert'
  2  import * as core from '@actions/core'
  3  import * as fs from 'fs'
  4  import * as github from '@actions/github'
  5  import * as io from '@actions/io'
  6  import * as path from 'path'
  7  import * as retryHelper from './retry-helper'
  8  import * as toolCache from '@actions/tool-cache'
  9  import {default as uuid} from 'uuid/v4'
 10  import {Octokit} from '@octokit/rest'
 11  
 12  const IS_WINDOWS = process.platform === 'win32'
 13  
 14  export async function downloadRepository(
 15    authToken: string,
 16    owner: string,
 17    repo: string,
 18    ref: string,
 19    commit: string,
 20    repositoryPath: string
 21  ): Promise<void> {
 22    // Determine the default branch
 23    if (!ref && !commit) {
 24      core.info('Determining the default branch')
 25      ref = await getDefaultBranch(authToken, owner, repo)
 26    }
 27  
 28    // Download the archive
 29    let archiveData = await retryHelper.execute(async () => {
 30      core.info('Downloading the archive')
 31      return await downloadArchive(authToken, owner, repo, ref, commit)
 32    })
 33  
 34    // Write archive to disk
 35    core.info('Writing archive to disk')
 36    const uniqueId = uuid()
 37    const archivePath = path.join(repositoryPath, `${uniqueId}.tar.gz`)
 38    await fs.promises.writeFile(archivePath, archiveData)
 39    archiveData = Buffer.from('') // Free memory
 40  
 41    // Extract archive
 42    core.info('Extracting the archive')
 43    const extractPath = path.join(repositoryPath, uniqueId)
 44    await io.mkdirP(extractPath)
 45    if (IS_WINDOWS) {
 46      await toolCache.extractZip(archivePath, extractPath)
 47    } else {
 48      await toolCache.extractTar(archivePath, extractPath)
 49    }
 50    await io.rmRF(archivePath)
 51  
 52    // Determine the path of the repository content. The archive contains
 53    // a top-level folder and the repository content is inside.
 54    const archiveFileNames = await fs.promises.readdir(extractPath)
 55    assert.ok(
 56      archiveFileNames.length == 1,
 57      'Expected exactly one directory inside archive'
 58    )
 59    const archiveVersion = archiveFileNames[0] // The top-level folder name includes the short SHA
 60    core.info(`Resolved version ${archiveVersion}`)
 61    const tempRepositoryPath = path.join(extractPath, archiveVersion)
 62  
 63    // Move the files
 64    for (const fileName of await fs.promises.readdir(tempRepositoryPath)) {
 65      const sourcePath = path.join(tempRepositoryPath, fileName)
 66      const targetPath = path.join(repositoryPath, fileName)
 67      if (IS_WINDOWS) {
 68        await io.cp(sourcePath, targetPath, {recursive: true}) // Copy on Windows (Windows Defender may have a lock)
 69      } else {
 70        await io.mv(sourcePath, targetPath)
 71      }
 72    }
 73    await io.rmRF(extractPath)
 74  }
 75  
 76  /**
 77   * Looks up the default branch name
 78   */
 79  export async function getDefaultBranch(
 80    authToken: string,
 81    owner: string,
 82    repo: string
 83  ): Promise<string> {
 84    return await retryHelper.execute(async () => {
 85      core.info('Retrieving the default branch name')
 86      const octokit = new github.GitHub(authToken)
 87      let result: string
 88      try {
 89        // Get the default branch from the repo info
 90        const response = await octokit.repos.get({owner, repo})
 91        result = response.data.default_branch
 92        assert.ok(result, 'default_branch cannot be empty')
 93      } catch (err) {
 94        // Handle .wiki repo
 95        if (err['status'] === 404 && repo.toUpperCase().endsWith('.WIKI')) {
 96          result = 'master'
 97        }
 98        // Otherwise error
 99        else {
100          throw err
101        }
102      }
103  
104      // Print the default branch
105      core.info(`Default branch '${result}'`)
106  
107      // Prefix with 'refs/heads'
108      if (!result.startsWith('refs/')) {
109        result = `refs/heads/${result}`
110      }
111  
112      return result
113    })
114  }
115  
116  async function downloadArchive(
117    authToken: string,
118    owner: string,
119    repo: string,
120    ref: string,
121    commit: string
122  ): Promise<Buffer> {
123    const octokit = new github.GitHub(authToken)
124    const params: Octokit.ReposGetArchiveLinkParams = {
125      owner: owner,
126      repo: repo,
127      archive_format: IS_WINDOWS ? 'zipball' : 'tarball',
128      ref: commit || ref
129    }
130    const response = await octokit.repos.getArchiveLink(params)
131    if (response.status != 200) {
132      throw new Error(
133        `Unexpected response from GitHub API. Status: ${response.status}, Data: ${response.data}`
134      )
135    }
136  
137    return Buffer.from(response.data) // response.data is ArrayBuffer
138  }