mirror of https://github.com/actions/toolkit
Merge pull request #1503 from actions/bethanyj28/download-artifact
Get a single artifact by name and download to `GITHUB_WORKSPACE`pull/1505/head^2
commit
62f943c0cc
|
@ -16,7 +16,9 @@
|
||||||
"@octokit/core": "^3.5.1",
|
"@octokit/core": "^3.5.1",
|
||||||
"@octokit/plugin-request-log": "^1.0.4",
|
"@octokit/plugin-request-log": "^1.0.4",
|
||||||
"@octokit/plugin-retry": "^3.0.9",
|
"@octokit/plugin-retry": "^3.0.9",
|
||||||
|
"@octokit/request-error": "^5.0.0",
|
||||||
"@protobuf-ts/plugin": "^2.2.3-alpha.1",
|
"@protobuf-ts/plugin": "^2.2.3-alpha.1",
|
||||||
|
"@types/unzipper": "^0.10.6",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
|
@ -292,6 +294,32 @@
|
||||||
"universal-user-agent": "^6.0.0"
|
"universal-user-agent": "^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@octokit/request-error": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-1ue0DH0Lif5iEqT52+Rf/hf0RmGO9NWFjrzmrkArpG9trFfDM/efx00BJHdLGuro4BR/gECxCU2Twf5OKrRFsQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/types": "^11.0.0",
|
||||||
|
"deprecation": "^2.0.0",
|
||||||
|
"once": "^1.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": {
|
||||||
|
"version": "18.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.0.0.tgz",
|
||||||
|
"integrity": "sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw=="
|
||||||
|
},
|
||||||
|
"node_modules/@octokit/request-error/node_modules/@octokit/types": {
|
||||||
|
"version": "11.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-11.1.0.tgz",
|
||||||
|
"integrity": "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/openapi-types": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@octokit/request/node_modules/@octokit/request-error": {
|
"node_modules/@octokit/request/node_modules/@octokit/request-error": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
|
||||||
|
@ -441,6 +469,14 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/unzipper": {
|
||||||
|
"version": "0.10.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.6.tgz",
|
||||||
|
"integrity": "sha512-zcBj329AHgKLQyz209N/S9R0GZqXSkUQO4tJSYE3x02qg4JuDFpgKMj50r82Erk1natCWQDIvSccDddt7jPzjA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/archiver": {
|
"node_modules/archiver": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz",
|
||||||
|
|
|
@ -46,7 +46,9 @@
|
||||||
"@octokit/core": "^3.5.1",
|
"@octokit/core": "^3.5.1",
|
||||||
"@octokit/plugin-request-log": "^1.0.4",
|
"@octokit/plugin-request-log": "^1.0.4",
|
||||||
"@octokit/plugin-retry": "^3.0.9",
|
"@octokit/plugin-retry": "^3.0.9",
|
||||||
|
"@octokit/request-error": "^5.0.0",
|
||||||
"@protobuf-ts/plugin": "^2.2.3-alpha.1",
|
"@protobuf-ts/plugin": "^2.2.3-alpha.1",
|
||||||
|
"@types/unzipper": "^0.10.6",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import path from 'path'
|
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import {PathLike} from 'fs'
|
|
||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as httpClient from '@actions/http-client'
|
import * as httpClient from '@actions/http-client'
|
||||||
|
@ -10,6 +8,7 @@ import {
|
||||||
DownloadArtifactResponse
|
DownloadArtifactResponse
|
||||||
} from '../shared/interfaces'
|
} from '../shared/interfaces'
|
||||||
import {getUserAgentString} from '../shared/user-agent'
|
import {getUserAgentString} from '../shared/user-agent'
|
||||||
|
import {getGitHubWorkspaceDir} from '../shared/config'
|
||||||
|
|
||||||
const scrubQueryParameters = (url: string): string => {
|
const scrubQueryParameters = (url: string): string => {
|
||||||
const parsed = new URL(url)
|
const parsed = new URL(url)
|
||||||
|
@ -30,7 +29,7 @@ async function exists(path: string): Promise<boolean> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function streamExtract(url: string, directory: PathLike): Promise<void> {
|
async function streamExtract(url: string, directory: string): Promise<void> {
|
||||||
const client = new httpClient.HttpClient(getUserAgentString())
|
const client = new httpClient.HttpClient(getUserAgentString())
|
||||||
const response = await client.get(url)
|
const response = await client.get(url)
|
||||||
|
|
||||||
|
@ -55,13 +54,12 @@ export async function downloadArtifact(
|
||||||
token: string,
|
token: string,
|
||||||
options?: DownloadArtifactOptions
|
options?: DownloadArtifactOptions
|
||||||
): Promise<DownloadArtifactResponse> {
|
): Promise<DownloadArtifactResponse> {
|
||||||
let downloadPath = options?.path || process.cwd() // TODO: make this align with GITHUB_WORKSPACE
|
const downloadPath = options?.path || getGitHubWorkspaceDir()
|
||||||
if (options?.createArtifactFolder) {
|
|
||||||
downloadPath = path.join(downloadPath, 'my-artifact') // TODO: need to pass artifact name
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await exists(downloadPath))) {
|
if (!(await exists(downloadPath))) {
|
||||||
core.debug(`Artifact destination folder does not exist, creating: ${downloadPath}`)
|
core.debug(
|
||||||
|
`Artifact destination folder does not exist, creating: ${downloadPath}`
|
||||||
|
)
|
||||||
await fs.mkdir(downloadPath, {recursive: true})
|
await fs.mkdir(downloadPath, {recursive: true})
|
||||||
} else {
|
} else {
|
||||||
core.debug(`Artifact destination folder already exists: ${downloadPath}`)
|
core.debug(`Artifact destination folder already exists: ${downloadPath}`)
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import {GetArtifactResponse} from '../shared/interfaces'
|
import {GetArtifactResponse} from '../shared/interfaces'
|
||||||
|
import {getOctokit} from '@actions/github'
|
||||||
|
import {getUserAgentString} from '../shared/user-agent'
|
||||||
|
import {defaults as defaultGitHubOptions} from '@actions/github/lib/utils'
|
||||||
|
import {getRetryOptions} from './retry-options'
|
||||||
|
import {requestLog} from '@octokit/plugin-request-log'
|
||||||
|
import {retry} from '@octokit/plugin-retry'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import {OctokitOptions} from '@octokit/core/dist-types/types'
|
||||||
|
|
||||||
export async function getArtifact(
|
export async function getArtifact(
|
||||||
artifactName: string,
|
artifactName: string,
|
||||||
|
@ -7,5 +15,55 @@ export async function getArtifact(
|
||||||
repositoryName: string,
|
repositoryName: string,
|
||||||
token: string
|
token: string
|
||||||
): Promise<GetArtifactResponse> {
|
): Promise<GetArtifactResponse> {
|
||||||
throw new Error('Not implemented')
|
const [retryOpts, requestOpts] = getRetryOptions(defaultGitHubOptions)
|
||||||
|
|
||||||
|
const opts: OctokitOptions = {
|
||||||
|
log: undefined,
|
||||||
|
userAgent: getUserAgentString(),
|
||||||
|
previews: undefined,
|
||||||
|
retry: retryOpts,
|
||||||
|
request: requestOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
const github = getOctokit(token, opts, retry, requestLog)
|
||||||
|
|
||||||
|
const getArtifactResp = await github.request(
|
||||||
|
'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts{?name}',
|
||||||
|
{
|
||||||
|
owner: repositoryOwner,
|
||||||
|
repo: repositoryName,
|
||||||
|
run_id: workflowRunId,
|
||||||
|
name: artifactName
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (getArtifactResp.status !== 200) {
|
||||||
|
core.warning(`non-200 response from GitHub API: ${getArtifactResp.status}`)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getArtifactResp.data.artifacts.length === 0) {
|
||||||
|
core.warning('no artifacts found')
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getArtifactResp.data.artifacts.length > 1) {
|
||||||
|
core.warning(
|
||||||
|
'more than one artifact found for a single name, returning first'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
artifact: {
|
||||||
|
name: getArtifactResp.data.artifacts[0].name,
|
||||||
|
id: getArtifactResp.data.artifacts[0].id,
|
||||||
|
url: getArtifactResp.data.artifacts[0].url,
|
||||||
|
size: getArtifactResp.data.artifacts[0].size_in_bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,26 +2,16 @@ import {info, warning, debug} from '@actions/core'
|
||||||
import {getOctokit} from '@actions/github'
|
import {getOctokit} from '@actions/github'
|
||||||
import {ListArtifactsResponse, Artifact} from '../shared/interfaces'
|
import {ListArtifactsResponse, Artifact} from '../shared/interfaces'
|
||||||
import {getUserAgentString} from '../shared/user-agent'
|
import {getUserAgentString} from '../shared/user-agent'
|
||||||
import {RetryOptions, getRetryOptions} from './retry-options'
|
import {getRetryOptions} from './retry-options'
|
||||||
import {defaults as defaultGitHubOptions} from '@actions/github/lib/utils'
|
import {defaults as defaultGitHubOptions} from '@actions/github/lib/utils'
|
||||||
import {requestLog} from '@octokit/plugin-request-log'
|
import {requestLog} from '@octokit/plugin-request-log'
|
||||||
import {retry} from '@octokit/plugin-retry'
|
import {retry} from '@octokit/plugin-retry'
|
||||||
import {RequestRequestOptions} from '@octokit/types'
|
import {OctokitOptions} from '@octokit/core/dist-types/types'
|
||||||
|
|
||||||
type Options = {
|
|
||||||
log?: Console
|
|
||||||
userAgent?: string
|
|
||||||
previews?: string[]
|
|
||||||
retry?: RetryOptions
|
|
||||||
request?: RequestRequestOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limiting to 1000 for perf reasons
|
// Limiting to 1000 for perf reasons
|
||||||
const maximumArtifactCount = 1000
|
const maximumArtifactCount = 1000
|
||||||
const paginationCount = 100
|
const paginationCount = 100
|
||||||
const maxNumberOfPages = maximumArtifactCount / paginationCount
|
const maxNumberOfPages = maximumArtifactCount / paginationCount
|
||||||
const maxRetryNumber = 5
|
|
||||||
const exemptStatusCodes = [400, 401, 403, 404, 422] // https://github.com/octokit/plugin-retry.js/blob/9a2443746c350b3beedec35cf26e197ea318a261/src/index.ts#L14
|
|
||||||
|
|
||||||
export async function listArtifacts(
|
export async function listArtifacts(
|
||||||
workflowRunId: number,
|
workflowRunId: number,
|
||||||
|
@ -29,16 +19,14 @@ export async function listArtifacts(
|
||||||
repositoryName: string,
|
repositoryName: string,
|
||||||
token: string
|
token: string
|
||||||
): Promise<ListArtifactsResponse> {
|
): Promise<ListArtifactsResponse> {
|
||||||
info(`Fetching artifact list for workflow run ${workflowRunId} in repository ${repositoryOwner}/${repositoryName}`)
|
info(
|
||||||
|
`Fetching artifact list for workflow run ${workflowRunId} in repository ${repositoryOwner}/${repositoryName}`
|
||||||
const artifacts: Artifact[] = []
|
|
||||||
const [retryOpts, requestOpts] = getRetryOptions(
|
|
||||||
maxRetryNumber,
|
|
||||||
exemptStatusCodes,
|
|
||||||
defaultGitHubOptions
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const opts: Options = {
|
const artifacts: Artifact[] = []
|
||||||
|
const [retryOpts, requestOpts] = getRetryOptions(defaultGitHubOptions)
|
||||||
|
|
||||||
|
const opts: OctokitOptions = {
|
||||||
log: undefined,
|
log: undefined,
|
||||||
userAgent: getUserAgentString(),
|
userAgent: getUserAgentString(),
|
||||||
previews: undefined,
|
previews: undefined,
|
||||||
|
@ -70,14 +58,14 @@ export async function listArtifacts(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate over the first page
|
// Iterate over the first page
|
||||||
listArtifactResponse.artifacts.forEach(artifact => {
|
for (const artifact of listArtifactResponse.artifacts) {
|
||||||
artifacts.push({
|
artifacts.push({
|
||||||
name: artifact.name,
|
name: artifact.name,
|
||||||
id: artifact.id,
|
id: artifact.id,
|
||||||
url: artifact.url,
|
url: artifact.url,
|
||||||
size: artifact.size_in_bytes
|
size: artifact.size_in_bytes
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
// Iterate over any remaining pages
|
// Iterate over any remaining pages
|
||||||
for (
|
for (
|
||||||
|
@ -97,19 +85,19 @@ export async function listArtifacts(
|
||||||
page: currentPageNumber
|
page: currentPageNumber
|
||||||
})
|
})
|
||||||
|
|
||||||
listArtifactResponse.artifacts.forEach(artifact => {
|
for (const artifact of listArtifactResponse.artifacts) {
|
||||||
artifacts.push({
|
artifacts.push({
|
||||||
name: artifact.name,
|
name: artifact.name,
|
||||||
id: artifact.id,
|
id: artifact.id,
|
||||||
url: artifact.url,
|
url: artifact.url,
|
||||||
size: artifact.size_in_bytes
|
size: artifact.size_in_bytes
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info(`Finished fetching artifact list`)
|
info(`Finished fetching artifact list`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
artifacts: artifacts
|
artifacts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,14 @@ export type RetryOptions = {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defaults for fetching artifacts
|
||||||
|
const defaultMaxRetryNumber = 5
|
||||||
|
const defaultExemptStatusCodes = [400, 401, 403, 404, 422] // https://github.com/octokit/plugin-retry.js/blob/9a2443746c350b3beedec35cf26e197ea318a261/src/index.ts#L14
|
||||||
|
|
||||||
export function getRetryOptions(
|
export function getRetryOptions(
|
||||||
retries: number,
|
defaultOptions: OctokitOptions,
|
||||||
exemptStatusCodes: number[],
|
retries: number = defaultMaxRetryNumber,
|
||||||
defaultOptions: OctokitOptions
|
exemptStatusCodes: number[] = defaultExemptStatusCodes
|
||||||
): [RetryOptions, RequestRequestOptions | undefined] {
|
): [RetryOptions, RequestRequestOptions | undefined] {
|
||||||
if (retries <= 0) {
|
if (retries <= 0) {
|
||||||
return [{enabled: false}, defaultOptions.request]
|
return [{enabled: false}, defaultOptions.request]
|
||||||
|
|
|
@ -26,3 +26,11 @@ export function isGhes(): boolean {
|
||||||
)
|
)
|
||||||
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
|
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getGitHubWorkspaceDir(): string {
|
||||||
|
const ghWorkspaceDir = process.env['GITHUB_WORKSPACE']
|
||||||
|
if (!ghWorkspaceDir) {
|
||||||
|
throw new Error('Unable to get the GITHUB_WORKSPACE env variable')
|
||||||
|
}
|
||||||
|
return ghWorkspaceDir
|
||||||
|
}
|
||||||
|
|
|
@ -91,12 +91,6 @@ export interface DownloadArtifactOptions {
|
||||||
* Denotes where the artifact will be downloaded to. If not specified then the artifact is download to GITHUB_WORKSPACE
|
* Denotes where the artifact will be downloaded to. If not specified then the artifact is download to GITHUB_WORKSPACE
|
||||||
*/
|
*/
|
||||||
path?: string
|
path?: string
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies if a root folder with the artifact name is created for the artifact that is downloaded
|
|
||||||
* Zip contents are expanded into this folder. Defaults to false if not specified
|
|
||||||
* */
|
|
||||||
createArtifactFolder?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*****************************************************************************
|
/*****************************************************************************
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
var packageJson = require('../../../package.json')
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
|
||||||
|
const packageJson = require('../../../package.json')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that this User Agent String is used in all HTTP calls so that we can monitor telemetry between different versions of this package
|
* Ensure that this User Agent String is used in all HTTP calls so that we can monitor telemetry between different versions of this package
|
||||||
|
|
|
@ -91,6 +91,6 @@ export async function uploadZipToBlobStorage(
|
||||||
return {
|
return {
|
||||||
isSuccess: true,
|
isSuccess: true,
|
||||||
uploadSize: uploadByteCount,
|
uploadSize: uploadByteCount,
|
||||||
md5Hash: md5Hash
|
md5Hash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ export async function uploadArtifact(
|
||||||
const createArtifactReq: CreateArtifactRequest = {
|
const createArtifactReq: CreateArtifactRequest = {
|
||||||
workflowRunBackendId: backendIds.workflowRunBackendId,
|
workflowRunBackendId: backendIds.workflowRunBackendId,
|
||||||
workflowJobRunBackendId: backendIds.workflowJobRunBackendId,
|
workflowJobRunBackendId: backendIds.workflowJobRunBackendId,
|
||||||
name: name,
|
name,
|
||||||
version: 4
|
version: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,13 +96,13 @@ export async function uploadArtifact(
|
||||||
const finalizeArtifactReq: FinalizeArtifactRequest = {
|
const finalizeArtifactReq: FinalizeArtifactRequest = {
|
||||||
workflowRunBackendId: backendIds.workflowRunBackendId,
|
workflowRunBackendId: backendIds.workflowRunBackendId,
|
||||||
workflowJobRunBackendId: backendIds.workflowJobRunBackendId,
|
workflowJobRunBackendId: backendIds.workflowJobRunBackendId,
|
||||||
name: name,
|
name,
|
||||||
size: uploadResult.uploadSize!.toString()
|
size: uploadResult.uploadSize ? uploadResult.uploadSize.toString() : '0'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uploadResult.md5Hash) {
|
if (uploadResult.md5Hash) {
|
||||||
finalizeArtifactReq.hash = StringValue.create({
|
finalizeArtifactReq.hash = StringValue.create({
|
||||||
value: `md5:${uploadResult.md5Hash!}`
|
value: `md5:${uploadResult.md5Hash}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue