github-api-helper.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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.js'
  8. import * as toolCache from '@actions/tool-cache'
  9. import {randomUUID} from 'crypto'
  10. import {getServerApiUrl} from './url-helper.js'
  11. const IS_WINDOWS = process.platform === 'win32'
  12. export interface RepositoryObjectFormatResult {
  13. format: string
  14. succeeded: boolean
  15. }
  16. export async function downloadRepository(
  17. authToken: string,
  18. owner: string,
  19. repo: string,
  20. ref: string,
  21. commit: string,
  22. repositoryPath: string,
  23. baseUrl?: string
  24. ): Promise<void> {
  25. // Determine the default branch
  26. if (!ref && !commit) {
  27. core.info('Determining the default branch')
  28. ref = await getDefaultBranch(authToken, owner, repo, baseUrl)
  29. }
  30. // Download the archive
  31. let archiveData = await retryHelper.execute(async () => {
  32. core.info('Downloading the archive')
  33. return await downloadArchive(authToken, owner, repo, ref, commit, baseUrl)
  34. })
  35. // Write archive to disk
  36. core.info('Writing archive to disk')
  37. const uniqueId = randomUUID()
  38. const archivePath = IS_WINDOWS
  39. ? path.join(repositoryPath, `${uniqueId}.zip`)
  40. : path.join(repositoryPath, `${uniqueId}.tar.gz`)
  41. await fs.promises.writeFile(archivePath, archiveData)
  42. archiveData = Buffer.from('') // Free memory
  43. // Extract archive
  44. core.info('Extracting the archive')
  45. const extractPath = path.join(repositoryPath, uniqueId)
  46. await io.mkdirP(extractPath)
  47. if (IS_WINDOWS) {
  48. await toolCache.extractZip(archivePath, extractPath)
  49. } else {
  50. await toolCache.extractTar(archivePath, extractPath)
  51. }
  52. await io.rmRF(archivePath)
  53. // Determine the path of the repository content. The archive contains
  54. // a top-level folder and the repository content is inside.
  55. const archiveFileNames = await fs.promises.readdir(extractPath)
  56. assert.ok(
  57. archiveFileNames.length == 1,
  58. 'Expected exactly one directory inside archive'
  59. )
  60. const archiveVersion = archiveFileNames[0] // The top-level folder name includes the short SHA
  61. core.info(`Resolved version ${archiveVersion}`)
  62. const tempRepositoryPath = path.join(extractPath, archiveVersion)
  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. * Looks up the default branch name
  77. */
  78. export async function getDefaultBranch(
  79. authToken: string,
  80. owner: string,
  81. repo: string,
  82. baseUrl?: string
  83. ): Promise<string> {
  84. return await retryHelper.execute(async () => {
  85. core.info('Retrieving the default branch name')
  86. const octokit = github.getOctokit(authToken, {
  87. baseUrl: getServerApiUrl(baseUrl)
  88. })
  89. let result: string
  90. try {
  91. // Get the default branch from the repo info
  92. const response = await octokit.rest.repos.get({owner, repo})
  93. result = response.data.default_branch
  94. assert.ok(result, 'default_branch cannot be empty')
  95. } catch (err) {
  96. // Handle .wiki repo
  97. if (
  98. (err as any)?.status === 404 &&
  99. repo.toUpperCase().endsWith('.WIKI')
  100. ) {
  101. result = 'master'
  102. }
  103. // Otherwise error
  104. else {
  105. throw err
  106. }
  107. }
  108. // Print the default branch
  109. core.info(`Default branch '${result}'`)
  110. // Prefix with 'refs/heads'
  111. if (!result.startsWith('refs/')) {
  112. result = `refs/heads/${result}`
  113. }
  114. return result
  115. })
  116. }
  117. export async function tryGetRepositoryObjectFormat(
  118. authToken: string,
  119. owner: string,
  120. repo: string,
  121. baseUrl?: string,
  122. commit?: string
  123. ): Promise<RepositoryObjectFormatResult> {
  124. const commitFormat = getObjectFormat(commit)
  125. if (commitFormat) {
  126. return {format: commitFormat, succeeded: true}
  127. }
  128. try {
  129. const octokit = github.getOctokit(authToken, {
  130. baseUrl: getServerApiUrl(baseUrl)
  131. })
  132. const response = await octokit.request(
  133. 'GET /repos/{owner}/{repo}/hash-algorithm',
  134. {owner, repo}
  135. )
  136. const hashAlgorithm = response.data.hash_algorithm
  137. if (hashAlgorithm === 'sha256' || hashAlgorithm === 'sha1') {
  138. return {format: hashAlgorithm, succeeded: true}
  139. }
  140. core.debug(
  141. 'Unable to determine repository object format from hash-algorithm endpoint'
  142. )
  143. return {format: '', succeeded: false}
  144. } catch (err) {
  145. core.debug(
  146. `Unable to determine repository object format from hash-algorithm endpoint: ${(err as any)?.message ?? err}`
  147. )
  148. return {format: '', succeeded: false}
  149. }
  150. }
  151. function getObjectFormat(sha?: string): string {
  152. if (/^[0-9a-fA-F]{64}$/.test(sha || '')) {
  153. return 'sha256'
  154. }
  155. if (/^[0-9a-fA-F]{40}$/.test(sha || '')) {
  156. return 'sha1'
  157. }
  158. return ''
  159. }
  160. async function downloadArchive(
  161. authToken: string,
  162. owner: string,
  163. repo: string,
  164. ref: string,
  165. commit: string,
  166. baseUrl?: string
  167. ): Promise<Buffer> {
  168. const octokit = github.getOctokit(authToken, {
  169. baseUrl: getServerApiUrl(baseUrl)
  170. })
  171. const download = IS_WINDOWS
  172. ? octokit.rest.repos.downloadZipballArchive
  173. : octokit.rest.repos.downloadTarballArchive
  174. const response = await download({
  175. owner: owner,
  176. repo: repo,
  177. ref: commit || ref
  178. })
  179. return Buffer.from(response.data as ArrayBuffer) // response.data is ArrayBuffer
  180. }