2023-08-21 21:23:54 +00:00
import fs from 'fs/promises'
2024-02-22 17:16:32 -05:00
import { createWriteStream } from 'fs'
import * as path from 'path'
2023-08-21 17:47:17 -04:00
import * as github from '@actions/github'
import * as core from '@actions/core'
2023-12-20 13:11:04 -05:00
import * as httpClient from '@actions/http-client'
2023-12-11 12:15:40 -05:00
import unzip from 'unzip-stream'
2023-08-17 14:40:33 -04:00
import {
DownloadArtifactOptions ,
DownloadArtifactResponse
} from '../shared/interfaces'
2023-12-20 13:11:04 -05:00
import { getUserAgentString } from '../shared/user-agent'
2023-08-22 11:47:14 -07:00
import { getGitHubWorkspaceDir } from '../shared/config'
2023-11-30 03:47:04 +00:00
import { internalArtifactTwirpClient } from '../shared/artifact-twirp-client'
2023-12-01 00:31:27 +00:00
import {
GetSignedArtifactURLRequest ,
2023-12-01 09:05:46 -05:00
Int64Value ,
2023-12-01 00:31:27 +00:00
ListArtifactsRequest
} from '../../generated'
2023-11-30 03:47:04 +00:00
import { getBackendIdsFromToken } from '../shared/util'
2023-12-05 18:35:26 +00:00
import { ArtifactNotFoundError } from '../shared/errors'
2023-12-18 17:11:14 -05:00
2023-08-21 21:23:54 +00:00
const scrubQueryParameters = ( url : string ) : string => {
const parsed = new URL ( url )
parsed . search = ''
return parsed . toString ()
}
async function exists ( path : string ) : Promise < boolean > {
try {
await fs . access ( path )
return true
} catch ( error ) {
if ( error . code === 'ENOENT' ) {
return false
} else {
throw error
}
}
}
2023-08-22 09:17:43 -07:00
async function streamExtract ( url : string , directory : string ) : Promise < void > {
2023-12-20 13:11:04 -05:00
let retryCount = 0
2023-12-20 15:45:19 -05:00
while ( retryCount < 5 ) {
2023-12-20 13:11:04 -05:00
try {
2024-01-09 16:44:12 +00:00
await streamExtractExternal ( url , directory )
2023-12-21 09:25:34 -05:00
return
2023-12-20 13:11:04 -05:00
} catch ( error ) {
retryCount ++
2024-01-09 19:36:26 +00:00
core . debug (
2023-12-21 09:25:34 -05:00
`Failed to download artifact after ${ retryCount } retries due to ${ error . message } . Retrying in 5 seconds...`
)
2023-12-20 15:45:19 -05:00
// wait 5 seconds before retrying
await new Promise ( resolve => setTimeout ( resolve , 5000 ))
2023-12-20 13:11:04 -05:00
}
2023-12-19 11:49:39 -05:00
}
2023-12-20 13:11:04 -05:00
throw new Error ( `Artifact download failed after ${ retryCount } retries.` )
}
2024-01-09 16:44:12 +00:00
export async function streamExtractExternal (
2023-12-21 09:25:34 -05:00
url : string ,
directory : string
) : Promise < void > {
2023-12-20 13:11:04 -05:00
const client = new httpClient . HttpClient ( getUserAgentString ())
const response = await client . get ( url )
if ( response . message . statusCode !== 200 ) {
throw new Error (
`Unexpected HTTP response from blob storage: ${ response . message . statusCode } ${ response . message . statusMessage } `
)
}
2023-12-19 11:49:39 -05:00
2023-12-20 18:08:00 -05:00
const timeout = 30 * 1000 // 30 seconds
2023-12-20 13:59:31 -05:00
2024-02-22 22:06:32 -05:00
return new Promise (( resolve , reject ) => {
2023-12-20 13:59:31 -05:00
const timerFn = () : void => {
2023-12-21 09:25:34 -05:00
response . message . destroy (
new Error ( `Blob storage chunk did not respond in ${ timeout } ms` )
)
2023-12-20 13:59:31 -05:00
}
2023-12-21 09:25:34 -05:00
const timer = setTimeout ( timerFn , timeout )
2023-12-20 13:59:31 -05:00
2024-02-22 20:29:42 -05:00
const promises : Promise < void >[] = []
2023-12-20 18:08:00 -05:00
response . message
. on ( 'data' , () => {
timer . refresh ()
})
. on ( 'error' , ( error : Error ) => {
2024-01-09 19:23:57 +00:00
core . debug (
2023-12-21 09:25:34 -05:00
`response.message: Artifact download failed: ${ error . message } `
)
2023-12-20 18:08:00 -05:00
clearTimeout ( timer )
reject ( error )
})
2024-02-22 17:16:32 -05:00
. pipe ( unzip . Parse ())
2024-02-22 22:06:32 -05:00
. on ( 'entry' , ( entry : unzip.Entry ) => {
2024-02-23 08:46:56 -05:00
const entryPath = path
. normalize ( entry . path )
. replace ( /^(\.\.(\/|\\|$))+/ , '' )
2024-02-23 08:42:23 -05:00
const fullPath = path . join ( directory , entryPath )
2024-02-22 17:16:32 -05:00
core . debug ( `Extracting artifact entry: ${ fullPath } ` )
2024-02-22 21:54:54 -05:00
if ( entry . type === 'Directory' ) {
2024-02-23 08:28:37 -05:00
promises . push ( resolveOrCreateDirectory ( fullPath ). then (() => {}))
2024-02-22 21:54:54 -05:00
entry . autodrain ()
} else {
2024-02-23 08:28:37 -05:00
promises . push (
resolveOrCreateDirectory ( path . dirname ( fullPath )). then (() => {})
)
2024-02-22 21:54:54 -05:00
const writeStream = createWriteStream ( fullPath )
promises . push (
new Promise (( resolve , reject ) => {
writeStream . on ( 'finish' , () => {
resolve ()
})
writeStream . on ( 'error' , reject )
})
)
entry . pipe ( writeStream )
}
2024-02-22 17:16:32 -05:00
})
2024-02-22 21:54:54 -05:00
. on ( 'end' , async () => {
2023-12-20 18:08:00 -05:00
clearTimeout ( timer )
2024-02-22 21:54:54 -05:00
try {
await Promise . all ( promises )
resolve ()
} catch ( error ) {
reject ( error )
}
2023-12-20 18:08:00 -05:00
})
. on ( 'error' , ( error : Error ) => {
reject ( error )
})
2023-12-11 12:15:40 -05:00
})
2023-08-21 21:23:54 +00:00
}
2023-08-17 14:40:33 -04:00
2023-11-30 03:47:04 +00:00
export async function downloadArtifactPublic (
2023-08-17 14:40:33 -04:00
artifactId : number ,
repositoryOwner : string ,
repositoryName : string ,
token : string ,
options? : DownloadArtifactOptions
) : Promise < DownloadArtifactResponse > {
2023-11-30 03:47:04 +00:00
const downloadPath = await resolveOrCreateDirectory ( options ? . path )
2023-08-21 21:23:54 +00:00
const api = github . getOctokit ( token )
core . info (
2023-08-21 17:47:17 -04:00
`Downloading artifact ' ${ artifactId } ' from ' ${ repositoryOwner } / ${ repositoryName } '`
2023-08-21 21:23:54 +00:00
)
const { headers , status } = await api . rest . actions . downloadArtifact ({
owner : repositoryOwner ,
repo : repositoryName ,
artifact_id : artifactId ,
archive_format : 'zip' ,
request : {
redirect : 'manual'
}
})
if ( status !== 302 ) {
throw new Error ( `Unable to download artifact. Unexpected status: ${ status } ` )
}
const { location } = headers
if ( ! location ) {
throw new Error ( `Unable to redirect to artifact download url` )
}
core . info (
`Redirecting to blob download url: ${ scrubQueryParameters ( location ) } `
)
try {
core . info ( `Starting download of artifact to: ${ downloadPath } ` )
await streamExtract ( location , downloadPath )
core . info ( `Artifact download completed successfully.` )
} catch ( error ) {
throw new Error ( `Unable to download and extract artifact: ${ error . message } ` )
}
2023-12-05 18:35:26 +00:00
return { downloadPath }
2023-08-17 14:40:33 -04:00
}
2023-11-30 03:47:04 +00:00
export async function downloadArtifactInternal (
artifactId : number ,
options? : DownloadArtifactOptions
) : Promise < DownloadArtifactResponse > {
const downloadPath = await resolveOrCreateDirectory ( options ? . path )
const artifactClient = internalArtifactTwirpClient ()
const { workflowRunBackendId , workflowJobRunBackendId } =
getBackendIdsFromToken ()
const listReq : ListArtifactsRequest = {
workflowRunBackendId ,
2023-12-01 09:05:46 -05:00
workflowJobRunBackendId ,
idFilter : Int64Value.create ({ value : artifactId.toString ()})
2023-11-30 03:47:04 +00:00
}
const { artifacts } = await artifactClient . ListArtifacts ( listReq )
if ( artifacts . length === 0 ) {
2023-12-05 18:35:26 +00:00
throw new ArtifactNotFoundError (
2023-11-30 03:47:04 +00:00
`No artifacts found for ID: ${ artifactId } \ nAre you trying to download from a different run? Try specifying a github-token with \`actions:read\` scope.`
)
}
if ( artifacts . length > 1 ) {
core . warning ( 'Multiple artifacts found, defaulting to first.' )
}
const signedReq : GetSignedArtifactURLRequest = {
workflowRunBackendId : artifacts [ 0 ]. workflowRunBackendId ,
workflowJobRunBackendId : artifacts [ 0 ]. workflowJobRunBackendId ,
name : artifacts [ 0 ]. name
}
const { signedUrl } = await artifactClient . GetSignedArtifactURL ( signedReq )
core . info (
`Redirecting to blob download url: ${ scrubQueryParameters ( signedUrl ) } `
)
try {
core . info ( `Starting download of artifact to: ${ downloadPath } ` )
await streamExtract ( signedUrl , downloadPath )
core . info ( `Artifact download completed successfully.` )
} catch ( error ) {
throw new Error ( `Unable to download and extract artifact: ${ error . message } ` )
}
2023-12-05 18:35:26 +00:00
return { downloadPath }
2023-11-30 03:47:04 +00:00
}
async function resolveOrCreateDirectory (
downloadPath = getGitHubWorkspaceDir ()
) : Promise < string > {
if ( ! ( await exists ( downloadPath ))) {
core . debug (
`Artifact destination folder does not exist, creating: ${ downloadPath } `
)
await fs . mkdir ( downloadPath , { recursive : true })
} else {
core . debug ( `Artifact destination folder already exists: ${ downloadPath } ` )
}
return downloadPath
}