2020-05-06 11:10:18 -04:00
import * as core from '@actions/core'
2024-11-27 05:58:22 -08:00
import { HttpClient } from '@actions/http-client'
import { BearerCredentialHandler } from '@actions/http-client/lib/auth'
2022-05-11 17:14:25 -04:00
import {
RequestOptions ,
TypedResponse
} from '@actions/http-client/lib/interfaces'
2020-05-06 11:10:18 -04:00
import * as fs from 'fs'
2024-11-27 05:58:22 -08:00
import { URL } from 'url'
2026-01-29 14:23:32 -05:00
import * as utils from './cacheUtils.js'
import { uploadCacheArchiveSDK } from './uploadUtils.js'
2020-05-06 11:10:18 -04:00
import {
ArtifactCacheEntry ,
2020-05-12 14:47:31 -04:00
InternalCacheOptions ,
2020-05-06 11:10:18 -04:00
CommitCacheRequest ,
ReserveCacheRequest ,
2022-04-04 16:21:58 +05:30
ReserveCacheResponse ,
2022-11-14 05:24:09 +00:00
ITypedResponseWithError ,
ArtifactCacheList
2026-01-29 14:23:32 -05:00
} from './contracts.js'
2023-08-07 13:25:56 -04:00
import {
downloadCacheHttpClient ,
downloadCacheHttpClientConcurrent ,
downloadCacheStorageSDK
2026-01-29 14:23:32 -05:00
} from './downloadUtils.js'
2020-07-10 10:09:32 -05:00
import {
DownloadOptions ,
UploadOptions ,
getDownloadOptions ,
getUploadOptions
2026-01-29 14:23:32 -05:00
} from '../options.js'
2020-07-10 10:09:32 -05:00
import {
isSuccessStatusCode ,
retryHttpClientResponse ,
retryTypedResponse
2026-01-29 14:23:32 -05:00
} from './requestUtils.js'
import { getCacheServiceURL } from './config.js'
import { getUserAgentString } from './shared/user-agent.js'
2020-05-06 11:10:18 -04:00
function getCacheApiUrl ( resource : string ) : string {
2024-09-24 03:29:14 -07:00
const baseUrl : string = getCacheServiceURL ()
2020-05-06 11:10:18 -04:00
if ( ! baseUrl ) {
throw new Error ( 'Cache Service Url not found, unable to restore cache.' )
}
const url = ` ${ baseUrl } _apis/artifactcache/ ${ resource } `
core . debug ( `Resource Url: ${ url } ` )
return url
}
function createAcceptHeader ( type : string , apiVersion : string ) : string {
return ` ${ type } ;api-version= ${ apiVersion } `
}
2022-05-11 17:14:25 -04:00
function getRequestOptions () : RequestOptions {
const requestOptions : RequestOptions = {
2020-05-06 11:10:18 -04:00
headers : {
Accept : createAcceptHeader ( 'application/json' , '6.0-preview.1' )
}
}
return requestOptions
}
function createHttpClient () : HttpClient {
const token = process . env [ 'ACTIONS_RUNTIME_TOKEN' ] || ''
const bearerCredentialHandler = new BearerCredentialHandler ( token )
return new HttpClient (
2024-11-27 05:50:01 -08:00
getUserAgentString (),
2020-05-06 11:10:18 -04:00
[ bearerCredentialHandler ],
getRequestOptions ()
)
}
export async function getCacheEntry (
keys : string [],
2020-05-06 17:53:22 -04:00
paths : string [],
2020-05-12 14:47:31 -04:00
options? : InternalCacheOptions
2020-05-06 11:10:18 -04:00
) : Promise < ArtifactCacheEntry | null > {
const httpClient = createHttpClient ()
2024-09-24 03:17:44 -07:00
const version = utils . getCacheVersion (
2023-01-04 12:16:25 +05:30
paths ,
options ? . compressionMethod ,
options ? . enableCrossOsArchive
)
2024-09-24 03:17:44 -07:00
2020-05-06 11:10:18 -04:00
const resource = `cache?keys= ${ encodeURIComponent (
keys . join ( ',' )
) } &version= ${ version } `
2020-05-12 12:37:03 -04:00
const response = await retryTypedResponse ( 'getCacheEntry' , async () =>
httpClient . getJson < ArtifactCacheEntry >( getCacheApiUrl ( resource ))
2020-05-06 11:10:18 -04:00
)
2023-01-04 12:16:25 +05:30
// Cache not found
2020-05-06 11:10:18 -04:00
if ( response . statusCode === 204 ) {
2022-12-13 11:25:40 +00:00
// List cache for primary key only if cache miss occurs
if ( core . isDebug ()) {
2022-12-21 10:24:52 +00:00
await printCachesListForDiagnostics ( keys [ 0 ], httpClient , version )
2022-12-13 11:25:40 +00:00
}
2020-05-06 11:10:18 -04:00
return null
}
if ( ! isSuccessStatusCode ( response . statusCode )) {
throw new Error ( `Cache service responded with ${ response . statusCode } ` )
}
const cacheResult = response . result
const cacheDownloadUrl = cacheResult ? . archiveLocation
if ( ! cacheDownloadUrl ) {
2023-01-04 12:16:25 +05:30
// Cache achiveLocation not found. This should never happen, and hence bail out.
2020-05-06 11:10:18 -04:00
throw new Error ( 'Cache not found.' )
}
core . setSecret ( cacheDownloadUrl )
core . debug ( `Cache Result:` )
core . debug ( JSON . stringify ( cacheResult ))
return cacheResult
}
2022-12-21 10:24:52 +00:00
async function printCachesListForDiagnostics (
2022-11-15 10:32:24 +00:00
key : string ,
httpClient : HttpClient ,
2022-12-13 11:48:30 +00:00
version : string
2022-11-15 10:32:24 +00:00
) : Promise < void > {
const resource = `caches?key= ${ encodeURIComponent ( key ) } `
const response = await retryTypedResponse ( 'listCache' , async () =>
httpClient . getJson < ArtifactCacheList >( getCacheApiUrl ( resource ))
)
2022-12-16 05:55:53 +00:00
if ( response . statusCode === 200 ) {
2022-11-15 10:32:24 +00:00
const cacheListResult = response . result
const totalCount = cacheListResult ? . totalCount
if ( totalCount && totalCount > 0 ) {
2022-12-13 11:25:40 +00:00
core . debug (
2022-12-16 18:17:37 +05:30
`No matching cache found for cache key ' ${ key } ', version ' ${ version } and scope ${ process . env [ 'GITHUB_REF' ] } . There exist one or more cache(s) with similar key but they have different version or scope. See more info on cache matching here: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#matching-a-cache-key \ nOther caches with similar key:`
2022-11-15 10:32:24 +00:00
)
2022-12-21 10:30:22 +00:00
for ( const cacheEntry of cacheListResult ? . artifactCaches || []) {
2022-11-15 10:32:24 +00:00
core . debug (
`Cache Key: ${ cacheEntry ? . cacheKey } , Cache Version: ${ cacheEntry ? . cacheVersion } , Cache Scope: ${ cacheEntry ? . scope } , Cache Created: ${ cacheEntry ? . creationTime } `
)
2022-12-13 11:25:40 +00:00
}
2022-11-15 10:32:24 +00:00
}
}
}
2020-05-06 11:10:18 -04:00
export async function downloadCache (
archiveLocation : string ,
2020-07-10 10:09:32 -05:00
archivePath : string ,
options? : DownloadOptions
2020-05-06 11:10:18 -04:00
) : Promise < void > {
2020-07-10 10:09:32 -05:00
const archiveUrl = new URL ( archiveLocation )
const downloadOptions = getDownloadOptions ( options )
2023-08-07 13:25:56 -04:00
if ( archiveUrl . hostname . endsWith ( '.blob.core.windows.net' )) {
if ( downloadOptions . useAzureSdk ) {
// Use Azure storage SDK to download caches hosted on Azure to improve speed and reliability.
await downloadCacheStorageSDK (
archiveLocation ,
archivePath ,
downloadOptions
)
} else if ( downloadOptions . concurrentBlobDownloads ) {
// Use concurrent implementation with HttpClient to work around blob SDK issue
await downloadCacheHttpClientConcurrent (
archiveLocation ,
archivePath ,
downloadOptions
)
} else {
// Otherwise, download using the Actions http-client.
await downloadCacheHttpClient ( archiveLocation , archivePath )
}
2020-05-06 11:10:18 -04:00
} else {
2020-07-10 10:09:32 -05:00
await downloadCacheHttpClient ( archiveLocation , archivePath )
2020-05-06 11:10:18 -04:00
}
}
// Reserve Cache
export async function reserveCache (
key : string ,
2020-05-06 17:53:22 -04:00
paths : string [],
2020-05-12 14:47:31 -04:00
options? : InternalCacheOptions
2022-04-04 16:21:58 +05:30
) : Promise < ITypedResponseWithError < ReserveCacheResponse >> {
2020-05-06 11:10:18 -04:00
const httpClient = createHttpClient ()
2024-09-24 03:17:44 -07:00
const version = utils . getCacheVersion (
2023-01-04 12:16:25 +05:30
paths ,
options ? . compressionMethod ,
options ? . enableCrossOsArchive
)
2020-05-06 11:10:18 -04:00
const reserveCacheRequest : ReserveCacheRequest = {
key ,
2022-04-04 16:21:58 +05:30
version ,
cacheSize : options?.cacheSize
2020-05-06 11:10:18 -04:00
}
2020-05-12 12:37:03 -04:00
const response = await retryTypedResponse ( 'reserveCache' , async () =>
httpClient . postJson < ReserveCacheResponse >(
getCacheApiUrl ( 'caches' ),
reserveCacheRequest
)
2020-05-06 11:10:18 -04:00
)
2022-04-04 16:21:58 +05:30
return response
2020-05-06 11:10:18 -04:00
}
function getContentRange ( start : number , end : number ) : string {
// Format: `bytes start-end/filesize
// start and end are inclusive
// filesize can be *
// For a 200 byte chunk starting at byte 0:
// Content-Range: bytes 0-199/*
return `bytes ${ start } - ${ end } /*`
}
async function uploadChunk (
httpClient : HttpClient ,
resourceUrl : string ,
2020-05-12 12:37:03 -04:00
openStream : () => NodeJS . ReadableStream ,
2020-05-06 11:10:18 -04:00
start : number ,
end : number
) : Promise < void > {
core . debug (
2024-11-27 05:58:22 -08:00
`Uploading chunk of size ${
end - start + 1
2023-08-03 16:36:11 -04:00
} bytes at offset ${ start } with content range: ${ getContentRange (
2020-05-06 11:10:18 -04:00
start ,
end
) } `
)
const additionalHeaders = {
'Content-Type' : 'application/octet-stream' ,
'Content-Range' : getContentRange ( start , end )
}
2020-11-03 09:53:44 -06:00
const uploadChunkResponse = await retryHttpClientResponse (
2020-05-12 12:37:03 -04:00
`uploadChunk (start: ${ start } , end: ${ end } )` ,
async () =>
httpClient . sendStream (
'PATCH' ,
resourceUrl ,
openStream (),
additionalHeaders
)
2020-05-06 11:10:18 -04:00
)
2020-11-03 09:53:44 -06:00
if ( ! isSuccessStatusCode ( uploadChunkResponse . message . statusCode )) {
throw new Error (
`Cache service responded with ${ uploadChunkResponse . message . statusCode } during upload chunk.`
)
}
2020-05-06 11:10:18 -04:00
}
async function uploadFile (
httpClient : HttpClient ,
cacheId : number ,
2020-05-12 12:37:03 -04:00
archivePath : string ,
options? : UploadOptions
2020-05-06 11:10:18 -04:00
) : Promise < void > {
// Upload Chunks
2021-05-03 18:09:44 +03:00
const fileSize = utils . getArchiveFileSizeInBytes ( archivePath )
2020-05-06 11:10:18 -04:00
const resourceUrl = getCacheApiUrl ( `caches/ ${ cacheId . toString () } ` )
const fd = fs . openSync ( archivePath , 'r' )
2020-07-10 10:09:32 -05:00
const uploadOptions = getUploadOptions ( options )
2020-05-06 11:10:18 -04:00
2020-07-10 10:09:32 -05:00
const concurrency = utils . assertDefined (
'uploadConcurrency' ,
uploadOptions . uploadConcurrency
)
const maxChunkSize = utils . assertDefined (
'uploadChunkSize' ,
uploadOptions . uploadChunkSize
)
2020-05-06 11:10:18 -04:00
const parallelUploads = [... new Array ( concurrency ). keys ()]
core . debug ( 'Awaiting all uploads' )
let offset = 0
try {
await Promise . all (
parallelUploads . map ( async () => {
while ( offset < fileSize ) {
2020-07-10 10:09:32 -05:00
const chunkSize = Math . min ( fileSize - offset , maxChunkSize )
2020-05-06 11:10:18 -04:00
const start = offset
const end = offset + chunkSize - 1
2020-07-10 10:09:32 -05:00
offset += maxChunkSize
2020-05-06 11:10:18 -04:00
2020-05-12 12:37:03 -04:00
await uploadChunk (
httpClient ,
resourceUrl ,
() =>
fs
. createReadStream ( archivePath , {
fd ,
start ,
end ,
autoClose : false
})
. on ( 'error' , error => {
throw new Error (
2020-07-10 10:09:32 -05:00
`Cache upload failed because file read failed with ${ error . message } `
2020-05-12 12:37:03 -04:00
)
}),
start ,
end
)
2020-05-06 11:10:18 -04:00
}
})
)
} finally {
fs . closeSync ( fd )
}
return
}
async function commitCache (
httpClient : HttpClient ,
cacheId : number ,
filesize : number
2022-05-11 17:14:25 -04:00
) : Promise < TypedResponse < null >> {
2024-11-27 05:58:22 -08:00
const commitCacheRequest : CommitCacheRequest = { size : filesize }
2020-05-12 12:37:03 -04:00
return await retryTypedResponse ( 'commitCache' , async () =>
httpClient . postJson < null >(
getCacheApiUrl ( `caches/ ${ cacheId . toString () } ` ),
commitCacheRequest
)
2020-05-06 11:10:18 -04:00
)
}
export async function saveCache (
cacheId : number ,
2020-05-12 12:37:03 -04:00
archivePath : string ,
2024-11-28 07:22:01 -08:00
signedUploadURL? : string ,
2020-05-12 12:37:03 -04:00
options? : UploadOptions
2020-05-06 11:10:18 -04:00
) : Promise < void > {
2024-11-28 07:22:01 -08:00
const uploadOptions = getUploadOptions ( options )
2020-05-06 11:10:18 -04:00
2024-11-28 07:22:01 -08:00
if ( uploadOptions . useAzureSdk ) {
// Use Azure storage SDK to upload caches directly to Azure
if ( ! signedUploadURL ) {
throw new Error (
'Azure Storage SDK can only be used when a signed URL is provided.'
)
}
await uploadCacheArchiveSDK ( signedUploadURL , archivePath , options )
} else {
const httpClient = createHttpClient ()
core . debug ( 'Upload cache' )
await uploadFile ( httpClient , cacheId , archivePath , options )
// Commit Cache
core . debug ( 'Commiting cache' )
const cacheSize = utils . getArchiveFileSizeInBytes ( archivePath )
core . info (
`Cache Size: ~ ${ Math . round (
cacheSize / ( 1024 * 1024 )
) } MB ( ${ cacheSize } B)`
)
2020-11-26 01:56:57 +03:00
2024-11-28 07:22:01 -08:00
const commitCacheResponse = await commitCache (
httpClient ,
cacheId ,
cacheSize
2020-05-06 11:10:18 -04:00
)
2024-11-28 07:22:01 -08:00
if ( ! isSuccessStatusCode ( commitCacheResponse . statusCode )) {
throw new Error (
`Cache service responded with ${ commitCacheResponse . statusCode } during commit cache.`
)
}
2020-05-06 11:10:18 -04:00
2024-11-28 07:22:01 -08:00
core . info ( 'Cache saved successfully' )
}
2020-05-06 11:10:18 -04:00
}