2020-05-06 15:10:18 +00:00
import * as core from '@actions/core'
2020-07-10 15:09:32 +00:00
import { HttpClient } from '@actions/http-client'
2022-05-11 21:14:25 +00:00
import { BearerCredentialHandler } from '@actions/http-client/lib/auth'
import {
RequestOptions ,
TypedResponse
} from '@actions/http-client/lib/interfaces'
2020-05-06 15:10:18 +00:00
import * as crypto from 'crypto'
import * as fs from 'fs'
2020-07-10 15:09:32 +00:00
import { URL } from 'url'
2020-05-06 15:10:18 +00:00
import * as utils from './cacheUtils'
2020-07-10 15:09:32 +00:00
import { CompressionMethod } from './constants'
2020-05-06 15:10:18 +00:00
import {
ArtifactCacheEntry ,
2020-05-12 18:47:31 +00:00
InternalCacheOptions ,
2020-05-06 15:10:18 +00:00
CommitCacheRequest ,
ReserveCacheRequest ,
2022-04-04 10:51:58 +00:00
ReserveCacheResponse ,
2022-11-14 05:24:09 +00:00
ITypedResponseWithError ,
ArtifactCacheList
2020-05-06 15:10:18 +00:00
} from './contracts'
2020-07-10 15:09:32 +00:00
import { downloadCacheHttpClient , downloadCacheStorageSDK } from './downloadUtils'
import {
DownloadOptions ,
UploadOptions ,
getDownloadOptions ,
getUploadOptions
} from '../options'
import {
isSuccessStatusCode ,
retryHttpClientResponse ,
retryTypedResponse
} from './requestUtils'
2020-05-06 15:10:18 +00:00
const versionSalt = '1.0'
function getCacheApiUrl ( resource : string ) : string {
2022-03-29 22:20:22 +00:00
const baseUrl : string = process . env [ 'ACTIONS_CACHE_URL' ] || ''
2020-05-06 15:10:18 +00: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 21:14:25 +00:00
function getRequestOptions ( ) : RequestOptions {
const requestOptions : RequestOptions = {
2020-05-06 15:10:18 +00: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 (
'actions/cache' ,
[ bearerCredentialHandler ] ,
getRequestOptions ( )
)
}
export function getCacheVersion (
2020-05-06 21:53:22 +00:00
paths : string [ ] ,
2020-05-06 15:10:18 +00:00
compressionMethod? : CompressionMethod
) : string {
2020-05-06 21:53:22 +00:00
const components = paths . concat (
2020-05-18 20:33:15 +00:00
! compressionMethod || compressionMethod === CompressionMethod . Gzip
? [ ]
: [ compressionMethod ]
2020-05-06 15:10:18 +00:00
)
// Add salt to cache version to support breaking changes in cache entry
components . push ( versionSalt )
return crypto
. createHash ( 'sha256' )
. update ( components . join ( '|' ) )
. digest ( 'hex' )
}
export async function getCacheEntry (
keys : string [ ] ,
2020-05-06 21:53:22 +00:00
paths : string [ ] ,
2020-05-12 18:47:31 +00:00
options? : InternalCacheOptions
2020-05-06 15:10:18 +00:00
) : Promise < ArtifactCacheEntry | null > {
const httpClient = createHttpClient ( )
2020-05-06 21:53:22 +00:00
const version = getCacheVersion ( paths , options ? . compressionMethod )
2020-05-06 15:10:18 +00:00
const resource = ` cache?keys= ${ encodeURIComponent (
keys . join ( ',' )
) } & version = $ { version } `
2020-05-12 16:37:03 +00:00
const response = await retryTypedResponse ( 'getCacheEntry' , async ( ) = >
httpClient . getJson < ArtifactCacheEntry > ( getCacheApiUrl ( resource ) )
2020-05-06 15:10:18 +00: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 15:10:18 +00: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 ) {
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 12:47:37 +00:00
` 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 15:10:18 +00:00
export async function downloadCache (
archiveLocation : string ,
2020-07-10 15:09:32 +00:00
archivePath : string ,
options? : DownloadOptions
2020-05-06 15:10:18 +00:00
) : Promise < void > {
2020-07-10 15:09:32 +00:00
const archiveUrl = new URL ( archiveLocation )
const downloadOptions = getDownloadOptions ( options )
if (
downloadOptions . useAzureSdk &&
archiveUrl . hostname . endsWith ( '.blob.core.windows.net' )
) {
// Use Azure storage SDK to download caches hosted on Azure to improve speed and reliability.
await downloadCacheStorageSDK ( archiveLocation , archivePath , downloadOptions )
2020-05-06 15:10:18 +00:00
} else {
2020-07-10 15:09:32 +00:00
// Otherwise, download using the Actions http-client.
await downloadCacheHttpClient ( archiveLocation , archivePath )
2020-05-06 15:10:18 +00:00
}
}
// Reserve Cache
export async function reserveCache (
key : string ,
2020-05-06 21:53:22 +00:00
paths : string [ ] ,
2020-05-12 18:47:31 +00:00
options? : InternalCacheOptions
2022-04-04 10:51:58 +00:00
) : Promise < ITypedResponseWithError < ReserveCacheResponse > > {
2020-05-06 15:10:18 +00:00
const httpClient = createHttpClient ( )
2020-05-06 21:53:22 +00:00
const version = getCacheVersion ( paths , options ? . compressionMethod )
2020-05-06 15:10:18 +00:00
const reserveCacheRequest : ReserveCacheRequest = {
key ,
2022-04-04 10:51:58 +00:00
version ,
cacheSize : options?.cacheSize
2020-05-06 15:10:18 +00:00
}
2020-05-12 16:37:03 +00:00
const response = await retryTypedResponse ( 'reserveCache' , async ( ) = >
httpClient . postJson < ReserveCacheResponse > (
getCacheApiUrl ( 'caches' ) ,
reserveCacheRequest
)
2020-05-06 15:10:18 +00:00
)
2022-04-04 10:51:58 +00:00
return response
2020-05-06 15:10:18 +00: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 16:37:03 +00:00
openStream : ( ) = > NodeJS . ReadableStream ,
2020-05-06 15:10:18 +00:00
start : number ,
end : number
) : Promise < void > {
core . debug (
` Uploading chunk of size ${ end -
start +
1 } bytes at offset $ { start } with content range : $ { getContentRange (
start ,
end
) } `
)
const additionalHeaders = {
'Content-Type' : 'application/octet-stream' ,
'Content-Range' : getContentRange ( start , end )
}
2020-11-03 15:53:44 +00:00
const uploadChunkResponse = await retryHttpClientResponse (
2020-05-12 16:37:03 +00:00
` uploadChunk (start: ${ start } , end: ${ end } ) ` ,
async ( ) = >
httpClient . sendStream (
'PATCH' ,
resourceUrl ,
openStream ( ) ,
additionalHeaders
)
2020-05-06 15:10:18 +00:00
)
2020-11-03 15:53:44 +00:00
if ( ! isSuccessStatusCode ( uploadChunkResponse . message . statusCode ) ) {
throw new Error (
` Cache service responded with ${ uploadChunkResponse . message . statusCode } during upload chunk. `
)
}
2020-05-06 15:10:18 +00:00
}
async function uploadFile (
httpClient : HttpClient ,
cacheId : number ,
2020-05-12 16:37:03 +00:00
archivePath : string ,
options? : UploadOptions
2020-05-06 15:10:18 +00:00
) : Promise < void > {
// Upload Chunks
2021-05-03 15:09:44 +00:00
const fileSize = utils . getArchiveFileSizeInBytes ( archivePath )
2020-05-06 15:10:18 +00:00
const resourceUrl = getCacheApiUrl ( ` caches/ ${ cacheId . toString ( ) } ` )
const fd = fs . openSync ( archivePath , 'r' )
2020-07-10 15:09:32 +00:00
const uploadOptions = getUploadOptions ( options )
2020-05-06 15:10:18 +00:00
2020-07-10 15:09:32 +00:00
const concurrency = utils . assertDefined (
'uploadConcurrency' ,
uploadOptions . uploadConcurrency
)
const maxChunkSize = utils . assertDefined (
'uploadChunkSize' ,
uploadOptions . uploadChunkSize
)
2020-05-06 15:10:18 +00: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 15:09:32 +00:00
const chunkSize = Math . min ( fileSize - offset , maxChunkSize )
2020-05-06 15:10:18 +00:00
const start = offset
const end = offset + chunkSize - 1
2020-07-10 15:09:32 +00:00
offset += maxChunkSize
2020-05-06 15:10:18 +00:00
2020-05-12 16:37:03 +00:00
await uploadChunk (
httpClient ,
resourceUrl ,
( ) = >
fs
. createReadStream ( archivePath , {
fd ,
start ,
end ,
autoClose : false
} )
. on ( 'error' , error = > {
throw new Error (
2020-07-10 15:09:32 +00:00
` Cache upload failed because file read failed with ${ error . message } `
2020-05-12 16:37:03 +00:00
)
} ) ,
start ,
end
)
2020-05-06 15:10:18 +00:00
}
} )
)
} finally {
fs . closeSync ( fd )
}
return
}
async function commitCache (
httpClient : HttpClient ,
cacheId : number ,
filesize : number
2022-05-11 21:14:25 +00:00
) : Promise < TypedResponse < null > > {
2020-05-06 15:10:18 +00:00
const commitCacheRequest : CommitCacheRequest = { size : filesize }
2020-05-12 16:37:03 +00:00
return await retryTypedResponse ( 'commitCache' , async ( ) = >
httpClient . postJson < null > (
getCacheApiUrl ( ` caches/ ${ cacheId . toString ( ) } ` ) ,
commitCacheRequest
)
2020-05-06 15:10:18 +00:00
)
}
export async function saveCache (
cacheId : number ,
2020-05-12 16:37:03 +00:00
archivePath : string ,
options? : UploadOptions
2020-05-06 15:10:18 +00:00
) : Promise < void > {
const httpClient = createHttpClient ( )
core . debug ( 'Upload cache' )
2020-05-12 16:37:03 +00:00
await uploadFile ( httpClient , cacheId , archivePath , options )
2020-05-06 15:10:18 +00:00
// Commit Cache
core . debug ( 'Commiting cache' )
2021-05-03 15:09:44 +00:00
const cacheSize = utils . getArchiveFileSizeInBytes ( archivePath )
2020-11-25 22:56:57 +00:00
core . info (
` Cache Size: ~ ${ Math . round ( cacheSize / ( 1024 * 1024 ) ) } MB ( ${ cacheSize } B) `
)
2020-05-06 15:10:18 +00:00
const commitCacheResponse = await commitCache ( httpClient , cacheId , cacheSize )
if ( ! isSuccessStatusCode ( commitCacheResponse . statusCode ) ) {
throw new Error (
` Cache service responded with ${ commitCacheResponse . statusCode } during commit cache. `
)
}
core . info ( 'Cache saved successfully' )
}