mirror of
https://github.com/robinraju/release-downloader.git
synced 2026-10-04 08:54:36 +03:00
Refactor downloader (#448)
* Refactor: Move downloader to a class * Use BearerCredentialHandler from typed-rest-client for authentication Add http response mock to unit tests
This commit is contained in:
1 parent
6785de63f4
commit
e0d223890b
11 files changed
+399
-285
No files matched your search
+53
-12
@@ -1,18 +1,59 @@
|
||||
import {IReleaseDownloadSettings} from "../src/download-settings"
|
||||
import {download} from "../src/download"
|
||||
import * as core from "@actions/core"
|
||||
import * as fs from "fs"
|
||||
import * as handlers from "typed-rest-client/Handlers"
|
||||
import * as io from "@actions/io"
|
||||
import * as thc from "typed-rest-client/HttpClient"
|
||||
|
||||
test("run download", async () => {
|
||||
|
||||
const githubtoken = process.env.GITHUB_TOKEN ?? ""
|
||||
import {IReleaseDownloadSettings} from "../src/download-settings"
|
||||
import {ReleaseDownloader} from "../src/release-downloader"
|
||||
|
||||
const nock = require("nock")
|
||||
|
||||
let downloader: ReleaseDownloader
|
||||
let httpClent: thc.HttpClient
|
||||
const outputFilePath = "./target"
|
||||
|
||||
beforeAll(() => {
|
||||
const githubtoken = process.env.REPO_TOKEN || ""
|
||||
|
||||
const credentialHandler = new handlers.BearerCredentialHandler(
|
||||
githubtoken,
|
||||
false
|
||||
)
|
||||
httpClent = new thc.HttpClient("gh-api-client", [credentialHandler])
|
||||
downloader = new ReleaseDownloader(httpClent)
|
||||
|
||||
nock("https://api.github.com")
|
||||
.get("/repos/robinraju/probable-potato/releases/latest")
|
||||
.reply(200, readFromFile("1-release-latest.json"))
|
||||
|
||||
nock("https://api.github.com", {
|
||||
reqheaders: {accept: "application/octet-stream"}
|
||||
})
|
||||
.get("/repos/robinraju/probable-potato/releases/assets/66946546")
|
||||
.replyWithFile(200, __dirname + "/resource/assets/test-1.txt")
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
io.rmRF(outputFilePath)
|
||||
})
|
||||
|
||||
function readFromFile(fileName: string): string {
|
||||
return fs.readFileSync(`./__tests__/resource/${fileName}`, {
|
||||
encoding: "utf-8"
|
||||
})
|
||||
}
|
||||
|
||||
test("Download from public repo", async () => {
|
||||
const downloadSettings: IReleaseDownloadSettings = {
|
||||
sourceRepoPath: "lihaoyi/Ammonite",
|
||||
isLatest: false,
|
||||
tag: "2.1.1",
|
||||
fileName: "2.13-2.1.1-14-4f2a1b2-bootstrap",
|
||||
sourceRepoPath: "robinraju/probable-potato",
|
||||
isLatest: true,
|
||||
tag: "",
|
||||
fileName: "*",
|
||||
tarBall: false,
|
||||
zipBall: false,
|
||||
outFilePath: "./target",
|
||||
token: githubtoken
|
||||
outFilePath: outputFilePath
|
||||
}
|
||||
await download(downloadSettings)
|
||||
const result = await downloader.download(downloadSettings)
|
||||
expect(result.length).toBe(1)
|
||||
}, 10000)
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"url": "https://api.github.com/repos/robinraju/probable-potato/releases/68092191",
|
||||
"assets_url": "https://api.github.com/repos/robinraju/probable-potato/releases/68092191/assets",
|
||||
"upload_url": "https://uploads.github.com/repos/robinraju/probable-potato/releases/68092191/assets{?name,label}",
|
||||
"html_url": "https://github.com/robinraju/probable-potato/releases/tag/1.0.1",
|
||||
"id": 68092191,
|
||||
"author": {},
|
||||
"node_id": "RE_kwDOHahpL84EDwEf",
|
||||
"tag_name": "1.0.1",
|
||||
"target_commitish": "main",
|
||||
"name": "Probable potato - v1.0.1",
|
||||
"draft": false,
|
||||
"prerelease": false,
|
||||
"created_at": "2022-05-29T12:03:47Z",
|
||||
"published_at": "2022-05-29T12:04:42Z",
|
||||
"assets": [
|
||||
{
|
||||
"url": "https://api.github.com/repos/robinraju/probable-potato/releases/assets/66946546",
|
||||
"id": 66946546,
|
||||
"node_id": "RA_kwDOHahpL84D_YXy",
|
||||
"name": "test-1.txt",
|
||||
"label": null,
|
||||
"uploader": {},
|
||||
"content_type": "image/jpeg",
|
||||
"state": "uploaded",
|
||||
"size": 7253,
|
||||
"download_count": 56,
|
||||
"created_at": "2022-05-29T12:08:23Z",
|
||||
"updated_at": "2022-05-29T12:08:23Z",
|
||||
"browser_download_url": "https://github.com/robinraju/probable-potato/releases/download/1.0.1/potato.jpeg"
|
||||
}
|
||||
],
|
||||
"tarball_url": "https://api.github.com/repos/robinraju/probable-potato/tarball/1.0.1",
|
||||
"zipball_url": "https://api.github.com/repos/robinraju/probable-potato/zipball/1.0.1",
|
||||
"body": ""
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
v1.0 robinraju/probable-potato/releases/latest
|
||||
+1
-1
@@ -16,7 +16,7 @@ inputs:
|
||||
required: false
|
||||
fileName:
|
||||
description: "Name of the file to download (use '*' to download all assets other than tarball or zipball)"
|
||||
default: ""
|
||||
default: "*"
|
||||
required: false
|
||||
tarBall:
|
||||
description: "Download tarball from assets"
|
||||
|
||||
Generated
+60
-10
@@ -24,6 +24,7 @@
|
||||
"jest": "^27.5.1",
|
||||
"jest-circus": "^28.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"nock": "^13.2.6",
|
||||
"prettier": "2.6.2",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.7.2"
|
||||
@@ -5551,6 +5552,12 @@
|
||||
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
@@ -5797,6 +5804,21 @@
|
||||
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nock": {
|
||||
"version": "13.2.6",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-13.2.6.tgz",
|
||||
"integrity": "sha512-GbyeSwSEP0FYouzETZ0l/XNm5tNcDNcXJKw3LCAb+mx8bZSwg1wEEvdL0FAyg5TkBJYiWSCtw6ag4XfmBy60FA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.1.0",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"propagate": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13"
|
||||
}
|
||||
},
|
||||
"node_modules/node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
@@ -6154,6 +6176,15 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/propagate": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
|
||||
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
|
||||
@@ -8613,8 +8644,7 @@
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
||||
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
"dev": true
|
||||
},
|
||||
"acorn-walk": {
|
||||
"version": "7.2.0",
|
||||
@@ -9335,8 +9365,7 @@
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz",
|
||||
"integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
"dev": true
|
||||
},
|
||||
"eslint-import-resolver-node": {
|
||||
"version": "0.3.6",
|
||||
@@ -9445,8 +9474,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-i18n-text/-/eslint-plugin-i18n-text-1.0.1.tgz",
|
||||
"integrity": "sha512-3G3UetST6rdqhqW9SfcfzNYMpQXS7wNkJvp6dsXnjzGiku6Iu5hl3B0kmk6lIcFPwYjhQIY+tXVRtK9TlGT7RA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
"dev": true
|
||||
},
|
||||
"eslint-plugin-import": {
|
||||
"version": "2.25.3",
|
||||
@@ -11079,8 +11107,7 @@
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
|
||||
"integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
"dev": true
|
||||
},
|
||||
"jest-regex-util": {
|
||||
"version": "27.5.1",
|
||||
@@ -11363,6 +11390,12 @@
|
||||
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
|
||||
"dev": true
|
||||
},
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"dev": true
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
@@ -11563,6 +11596,18 @@
|
||||
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
|
||||
"dev": true
|
||||
},
|
||||
"nock": {
|
||||
"version": "13.2.6",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-13.2.6.tgz",
|
||||
"integrity": "sha512-GbyeSwSEP0FYouzETZ0l/XNm5tNcDNcXJKw3LCAb+mx8bZSwg1wEEvdL0FAyg5TkBJYiWSCtw6ag4XfmBy60FA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^4.1.0",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"propagate": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
@@ -11820,6 +11865,12 @@
|
||||
"sisteransi": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"propagate": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
|
||||
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
|
||||
"dev": true
|
||||
},
|
||||
"psl": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
|
||||
@@ -12500,8 +12551,7 @@
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
|
||||
"integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
"dev": true
|
||||
},
|
||||
"xml-name-validator": {
|
||||
"version": "3.0.0",
|
||||
|
||||
+2
-1
@@ -30,8 +30,8 @@
|
||||
"typed-rest-client": "^1.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^17.0.36",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^17.0.36",
|
||||
"@typescript-eslint/parser": "^5.27.0",
|
||||
"@vercel/ncc": "^0.33.4",
|
||||
"eslint": "^8.16.0",
|
||||
@@ -40,6 +40,7 @@
|
||||
"jest": "^27.5.1",
|
||||
"jest-circus": "^28.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"nock": "^13.2.6",
|
||||
"prettier": "2.6.2",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.7.2"
|
||||
|
||||
@@ -33,9 +33,4 @@ export interface IReleaseDownloadSettings {
|
||||
* Target path to download the file
|
||||
*/
|
||||
outFilePath: string
|
||||
|
||||
/**
|
||||
* Github access token to download from private repos (Optional)
|
||||
*/
|
||||
token: string
|
||||
}
|
||||
-252
@@ -1,252 +0,0 @@
|
||||
import * as core from "@actions/core"
|
||||
import * as fs from "fs"
|
||||
import * as io from "@actions/io"
|
||||
import * as path from "path"
|
||||
import * as thc from "typed-rest-client/HttpClient"
|
||||
|
||||
import {DownloadMetaData, GithubRelease} from "./gh-api"
|
||||
import {IHeaders, IHttpClientResponse} from "typed-rest-client/Interfaces"
|
||||
|
||||
import {IReleaseDownloadSettings} from "./download-settings"
|
||||
|
||||
const API_ROOT = "https://api.github.com/repos"
|
||||
const httpClient: thc.HttpClient = new thc.HttpClient("gh-api-client")
|
||||
|
||||
export async function download(
|
||||
_downloadSettings: IReleaseDownloadSettings
|
||||
): Promise<string[]> {
|
||||
let ghRelease: GithubRelease
|
||||
|
||||
if (_downloadSettings.isLatest) {
|
||||
ghRelease = await getlatestRelease(
|
||||
_downloadSettings.sourceRepoPath,
|
||||
_downloadSettings.token
|
||||
)
|
||||
} else {
|
||||
ghRelease = await getReleaseByTag(
|
||||
_downloadSettings.sourceRepoPath,
|
||||
_downloadSettings.tag,
|
||||
_downloadSettings.token
|
||||
)
|
||||
}
|
||||
|
||||
const resolvedAssets: DownloadMetaData[] = resolveAssets(
|
||||
ghRelease,
|
||||
_downloadSettings
|
||||
)
|
||||
|
||||
return await downloadReleaseAssets(
|
||||
resolvedAssets,
|
||||
_downloadSettings.outFilePath,
|
||||
_downloadSettings.token
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latest release metadata from github api
|
||||
* @param repoPath The source repository path. {owner}/{repo}
|
||||
*/
|
||||
async function getlatestRelease(
|
||||
repoPath: string,
|
||||
token: string
|
||||
): Promise<GithubRelease> {
|
||||
core.info(`Fetching latest release for repo ${repoPath}`)
|
||||
|
||||
const headers: IHeaders = {Accept: "application/vnd.github.v3+json"}
|
||||
if (token !== "") {
|
||||
headers["Authorization"] = `token ${token}`
|
||||
}
|
||||
|
||||
const response = await httpClient.get(
|
||||
`${API_ROOT}/${repoPath}/releases/latest`,
|
||||
headers
|
||||
)
|
||||
|
||||
if (response.message.statusCode !== 200) {
|
||||
const err: Error = new Error(
|
||||
`[getlatestRelease] Unexpected response: ${response.message.statusCode}`
|
||||
)
|
||||
throw err
|
||||
}
|
||||
|
||||
const responseBody = await response.readBody()
|
||||
const _release: GithubRelease = JSON.parse(responseBody.toString())
|
||||
|
||||
core.info(`Found latest release version: ${_release.tag_name}`)
|
||||
return _release
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets release data of the specified tag
|
||||
* @param repoPath The source repository
|
||||
* @param tag The github tag to fetch release from.
|
||||
*/
|
||||
async function getReleaseByTag(
|
||||
repoPath: string,
|
||||
tag: string,
|
||||
token: string
|
||||
): Promise<GithubRelease> {
|
||||
core.info(`Fetching release ${tag} from repo ${repoPath}`)
|
||||
|
||||
if (tag === "") {
|
||||
throw new Error("Config error: Please input a valid tag")
|
||||
}
|
||||
|
||||
const headers: IHeaders = {Accept: "application/vnd.github.v3+json"}
|
||||
if (token !== "") {
|
||||
headers["Authorization"] = `token ${token}`
|
||||
}
|
||||
|
||||
const response = await httpClient.get(
|
||||
`${API_ROOT}/${repoPath}/releases/tags/${tag}`,
|
||||
headers
|
||||
)
|
||||
|
||||
if (response.message.statusCode !== 200) {
|
||||
const err: Error = new Error(
|
||||
`[getReleaseByTag] Unexpected response: ${response.message.statusCode}`
|
||||
)
|
||||
throw err
|
||||
}
|
||||
|
||||
const responseBody = await response.readBody()
|
||||
const _release: GithubRelease = JSON.parse(responseBody.toString())
|
||||
|
||||
return _release
|
||||
}
|
||||
|
||||
function resolveAssets(
|
||||
_release: GithubRelease,
|
||||
_settings: IReleaseDownloadSettings
|
||||
): DownloadMetaData[] {
|
||||
const downloads: DownloadMetaData[] = []
|
||||
|
||||
if (_release && _release.assets.length > 0) {
|
||||
if (_settings.fileName === "*") {
|
||||
// Download all assets
|
||||
for (const asset of _release.assets) {
|
||||
const dData: DownloadMetaData = {
|
||||
fileName: asset["name"],
|
||||
url: asset["url"],
|
||||
isTarBallOrZipBall: false
|
||||
}
|
||||
downloads.push(dData)
|
||||
}
|
||||
} else {
|
||||
const asset = _release.assets.find(a => a.name === _settings.fileName)
|
||||
if (asset) {
|
||||
const dData: DownloadMetaData = {
|
||||
fileName: asset["name"],
|
||||
url: asset["url"],
|
||||
isTarBallOrZipBall: false
|
||||
}
|
||||
downloads.push(dData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_settings.tarBall) {
|
||||
const fName = _settings.sourceRepoPath.split("/")[1]
|
||||
downloads.push({
|
||||
fileName: `${fName}-${_release.name}.tar.gz`,
|
||||
url: _release.tarball_url,
|
||||
isTarBallOrZipBall: true
|
||||
})
|
||||
}
|
||||
|
||||
if (_settings.zipBall) {
|
||||
const fName = _settings.sourceRepoPath.split("/")[1]
|
||||
downloads.push({
|
||||
fileName: `${fName}-${_release.name}.zip`,
|
||||
url: _release.zipball_url,
|
||||
isTarBallOrZipBall: true
|
||||
})
|
||||
}
|
||||
|
||||
return downloads
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the specified assets from a given URL
|
||||
* @param dData The download metadata
|
||||
* @param out Target directory
|
||||
* @param token Personal access token to for private repos
|
||||
*/
|
||||
async function downloadReleaseAssets(
|
||||
dData: DownloadMetaData[],
|
||||
out: string,
|
||||
token: string
|
||||
): Promise<string[]> {
|
||||
const outFileDir = path.resolve(out)
|
||||
|
||||
if (!fs.existsSync(outFileDir)) {
|
||||
io.mkdirP(outFileDir)
|
||||
}
|
||||
|
||||
const downloads: Promise<string>[] = []
|
||||
|
||||
for (const asset of dData) {
|
||||
downloads.push(downloadFile(asset, out, token))
|
||||
}
|
||||
|
||||
const result = await Promise.all(downloads)
|
||||
return result
|
||||
}
|
||||
|
||||
async function downloadFile(
|
||||
asset: DownloadMetaData,
|
||||
outputPath: string,
|
||||
token: string
|
||||
): Promise<string> {
|
||||
// Temporary fix for https://github.com/microsoft/typed-rest-client/issues/302
|
||||
const ghClient: thc.HttpClient = new thc.HttpClient("gh-api-client", [], {
|
||||
allowRedirects: false
|
||||
})
|
||||
|
||||
const headers: IHeaders = {
|
||||
Accept: "application/octet-stream"
|
||||
}
|
||||
|
||||
if (asset.isTarBallOrZipBall) {
|
||||
headers["Accept"] = "*/*"
|
||||
}
|
||||
|
||||
if (token !== "") {
|
||||
headers["Authorization"] = `token ${token}`
|
||||
}
|
||||
|
||||
core.info(`Downloading file: ${asset.fileName} to: ${outputPath}`)
|
||||
const response = await ghClient.get(asset.url, headers)
|
||||
|
||||
if (response.message.statusCode === 200) {
|
||||
return saveFile(outputPath, asset.fileName, response)
|
||||
} else if (response.message.statusCode === 302) {
|
||||
delete headers["Authorization"]
|
||||
const assetLocation = response.message.headers.location as string
|
||||
const assetResponse = await ghClient.get(assetLocation, headers)
|
||||
return saveFile(outputPath, asset.fileName, assetResponse)
|
||||
} else {
|
||||
const err: Error = new Error(
|
||||
`Unexpected response: ${response.message.statusCode}`
|
||||
)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFile(
|
||||
outputPath: string,
|
||||
fileName: string,
|
||||
httpClientResponse: IHttpClientResponse
|
||||
): Promise<string> {
|
||||
const outFilePath: string = path.resolve(outputPath, fileName)
|
||||
const fileStream: fs.WriteStream = fs.createWriteStream(outFilePath)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fileStream.on("error", err => reject(err))
|
||||
const outStream = httpClientResponse.message.pipe(fileStream)
|
||||
|
||||
outStream.on("close", () => {
|
||||
resolve(outFilePath)
|
||||
})
|
||||
})
|
||||
}
|
||||
+1
-2
@@ -1,5 +1,6 @@
|
||||
import * as core from "@actions/core"
|
||||
import * as path from "path"
|
||||
|
||||
import {IReleaseDownloadSettings} from "./download-settings"
|
||||
|
||||
export function getInputs(): IReleaseDownloadSettings {
|
||||
@@ -42,7 +43,5 @@ export function getInputs(): IReleaseDownloadSettings {
|
||||
const outFilePath = core.getInput("out-file-path") || "."
|
||||
downloadSettings.outFilePath = path.resolve(githubWorkspacePath, outFilePath)
|
||||
|
||||
downloadSettings.token = core.getInput("token")
|
||||
|
||||
return downloadSettings
|
||||
}
|
||||
+15
-2
@@ -1,13 +1,26 @@
|
||||
import * as core from "@actions/core"
|
||||
import * as handlers from "typed-rest-client/Handlers"
|
||||
import * as inputHelper from "./input-helper"
|
||||
import * as thc from "typed-rest-client/HttpClient"
|
||||
|
||||
import {download} from "./download"
|
||||
import {ReleaseDownloader} from "./release-downloader"
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const downloadSettings = inputHelper.getInputs()
|
||||
const authToken = core.getInput("token")
|
||||
|
||||
const res: string[] = await download(downloadSettings)
|
||||
const credentialHandler = new handlers.BearerCredentialHandler(
|
||||
authToken,
|
||||
false
|
||||
)
|
||||
const httpClient: thc.HttpClient = new thc.HttpClient("gh-api-client", [
|
||||
credentialHandler
|
||||
])
|
||||
|
||||
const downloader = new ReleaseDownloader(httpClient)
|
||||
|
||||
const res: string[] = await downloader.download(downloadSettings)
|
||||
core.info(`Done: ${res}`)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import * as core from "@actions/core"
|
||||
import * as fs from "fs"
|
||||
import * as io from "@actions/io"
|
||||
import * as path from "path"
|
||||
import * as thc from "typed-rest-client/HttpClient"
|
||||
|
||||
import {DownloadMetaData, GithubRelease} from "./gh-api"
|
||||
import {IHeaders, IHttpClientResponse} from "typed-rest-client/Interfaces"
|
||||
|
||||
import {IReleaseDownloadSettings} from "./download-settings"
|
||||
|
||||
export class ReleaseDownloader {
|
||||
private httpClient: thc.HttpClient
|
||||
|
||||
private _apiRoot = "https://api.github.com/repos"
|
||||
|
||||
constructor(httpClient: thc.HttpClient) {
|
||||
this.httpClient = httpClient
|
||||
}
|
||||
|
||||
async download(
|
||||
downloadSettings: IReleaseDownloadSettings
|
||||
): Promise<string[]> {
|
||||
let ghRelease: GithubRelease
|
||||
|
||||
if (downloadSettings.isLatest) {
|
||||
ghRelease = await this.getlatestRelease(downloadSettings.sourceRepoPath)
|
||||
} else {
|
||||
ghRelease = await this.getReleaseByTag(
|
||||
downloadSettings.sourceRepoPath,
|
||||
downloadSettings.tag
|
||||
)
|
||||
}
|
||||
|
||||
const resolvedAssets: DownloadMetaData[] = this.resolveAssets(
|
||||
ghRelease,
|
||||
downloadSettings
|
||||
)
|
||||
|
||||
return await this.downloadReleaseAssets(
|
||||
resolvedAssets,
|
||||
downloadSettings.outFilePath
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latest release metadata from github api
|
||||
* @param repoPath The source repository path. {owner}/{repo}
|
||||
*/
|
||||
private async getlatestRelease(repoPath: string): Promise<GithubRelease> {
|
||||
core.info(`Fetching latest release for repo ${repoPath}`)
|
||||
|
||||
const headers: IHeaders = {Accept: "application/vnd.github.v3+json"}
|
||||
|
||||
const response = await this.httpClient.get(
|
||||
`${this._apiRoot}/${repoPath}/releases/latest`,
|
||||
headers
|
||||
)
|
||||
|
||||
if (response.message.statusCode !== 200) {
|
||||
const err: Error = new Error(
|
||||
`[getlatestRelease] Unexpected response: ${response.message.statusCode}`
|
||||
)
|
||||
throw err
|
||||
}
|
||||
|
||||
const responseBody = await response.readBody()
|
||||
const release: GithubRelease = JSON.parse(responseBody.toString())
|
||||
|
||||
core.info(`Found latest release version: ${release.tag_name}`)
|
||||
return release
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets release data of the specified tag
|
||||
* @param repoPath The source repository
|
||||
* @param tag The github tag to fetch release from.
|
||||
*/
|
||||
private async getReleaseByTag(
|
||||
repoPath: string,
|
||||
tag: string
|
||||
): Promise<GithubRelease> {
|
||||
core.info(`Fetching release ${tag} from repo ${repoPath}`)
|
||||
|
||||
if (tag === "") {
|
||||
throw new Error("Config error: Please input a valid tag")
|
||||
}
|
||||
|
||||
const headers: IHeaders = {Accept: "application/vnd.github.v3+json"}
|
||||
|
||||
const response = await this.httpClient.get(
|
||||
`${this._apiRoot}/${repoPath}/releases/tags/${tag}`,
|
||||
headers
|
||||
)
|
||||
|
||||
if (response.message.statusCode !== 200) {
|
||||
const err: Error = new Error(
|
||||
`[getReleaseByTag] Unexpected response: ${response.message.statusCode}`
|
||||
)
|
||||
throw err
|
||||
}
|
||||
|
||||
const responseBody = await response.readBody()
|
||||
const release: GithubRelease = JSON.parse(responseBody.toString())
|
||||
core.info(`Found release tag: ${release.tag_name}`)
|
||||
|
||||
return release
|
||||
}
|
||||
|
||||
private resolveAssets(
|
||||
ghRelease: GithubRelease,
|
||||
downloadSettings: IReleaseDownloadSettings
|
||||
): DownloadMetaData[] {
|
||||
const downloads: DownloadMetaData[] = []
|
||||
|
||||
if (ghRelease && ghRelease.assets.length > 0) {
|
||||
if (downloadSettings.fileName === "*") {
|
||||
// Download all assets
|
||||
for (const asset of ghRelease.assets) {
|
||||
const dData: DownloadMetaData = {
|
||||
fileName: asset["name"],
|
||||
url: asset["url"],
|
||||
isTarBallOrZipBall: false
|
||||
}
|
||||
downloads.push(dData)
|
||||
}
|
||||
} else {
|
||||
const asset = ghRelease.assets.find(
|
||||
a => a.name === downloadSettings.fileName
|
||||
)
|
||||
if (asset) {
|
||||
const dData: DownloadMetaData = {
|
||||
fileName: asset["name"],
|
||||
url: asset["url"],
|
||||
isTarBallOrZipBall: false
|
||||
}
|
||||
downloads.push(dData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadSettings.tarBall) {
|
||||
const fName = downloadSettings.sourceRepoPath.split("/")[1]
|
||||
downloads.push({
|
||||
fileName: `${fName}-${ghRelease.name}.tar.gz`,
|
||||
url: ghRelease.tarball_url,
|
||||
isTarBallOrZipBall: true
|
||||
})
|
||||
}
|
||||
|
||||
if (downloadSettings.zipBall) {
|
||||
const fName = downloadSettings.sourceRepoPath.split("/")[1]
|
||||
downloads.push({
|
||||
fileName: `${fName}-${ghRelease.name}.zip`,
|
||||
url: ghRelease.zipball_url,
|
||||
isTarBallOrZipBall: true
|
||||
})
|
||||
}
|
||||
|
||||
return downloads
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the specified assets from a given URL
|
||||
* @param dData The download metadata
|
||||
* @param out Target directory
|
||||
*/
|
||||
private async downloadReleaseAssets(
|
||||
dData: DownloadMetaData[],
|
||||
out: string
|
||||
): Promise<string[]> {
|
||||
const outFileDir = path.resolve(out)
|
||||
|
||||
if (!fs.existsSync(outFileDir)) {
|
||||
io.mkdirP(outFileDir)
|
||||
}
|
||||
|
||||
const downloads: Promise<string>[] = []
|
||||
|
||||
for (const asset of dData) {
|
||||
downloads.push(this.downloadFile(asset, out))
|
||||
}
|
||||
|
||||
const result = await Promise.all(downloads)
|
||||
return result
|
||||
}
|
||||
|
||||
private async downloadFile(
|
||||
asset: DownloadMetaData,
|
||||
outputPath: string
|
||||
): Promise<string> {
|
||||
const headers: IHeaders = {
|
||||
Accept: "application/octet-stream"
|
||||
}
|
||||
|
||||
if (asset.isTarBallOrZipBall) {
|
||||
headers["Accept"] = "*/*"
|
||||
}
|
||||
|
||||
core.info(`Downloading file: ${asset.fileName} to: ${outputPath}`)
|
||||
const response = await this.httpClient.get(asset.url, headers)
|
||||
|
||||
if (response.message.statusCode === 200) {
|
||||
return this.saveFile(outputPath, asset.fileName, response)
|
||||
} else {
|
||||
const err: Error = new Error(
|
||||
`Unexpected response: ${response.message.statusCode}`
|
||||
)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
private async saveFile(
|
||||
outputPath: string,
|
||||
fileName: string,
|
||||
httpClientResponse: IHttpClientResponse
|
||||
): Promise<string> {
|
||||
const outFilePath: string = path.resolve(outputPath, fileName)
|
||||
const fileStream: fs.WriteStream = fs.createWriteStream(outFilePath)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fileStream.on("error", err => reject(err))
|
||||
const outStream = httpClientResponse.message.pipe(fileStream)
|
||||
|
||||
outStream.on("close", () => {
|
||||
resolve(outFilePath)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user