1
0
Fork 0

Merge pull request #1503 from actions/bethanyj28/download-artifact

Get a single artifact by name and download to `GITHUB_WORKSPACE`
pull/1505/head^2
Bethany 2023-08-23 14:11:06 -04:00 committed by GitHub
commit 62f943c0cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 138 additions and 49 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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}`)

View File

@ -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
}
}
} }

View File

@ -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
} }
} }

View File

@ -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]

View File

@ -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
}

View File

@ -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
} }
/***************************************************************************** /*****************************************************************************

View File

@ -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

View File

@ -91,6 +91,6 @@ export async function uploadZipToBlobStorage(
return { return {
isSuccess: true, isSuccess: true,
uploadSize: uploadByteCount, uploadSize: uploadByteCount,
md5Hash: md5Hash md5Hash
} }
} }

View File

@ -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}`
}) })
} }