2023-11-17 06:45:51 +00:00
import * as core from '@actions/core'
import { HttpClient } from '@actions/http-client'
import { BearerCredentialHandler } from '@actions/http-client/lib/auth'
import {
RequestOptions ,
TypedResponse
} from '@actions/http-client/lib/interfaces'
import * as crypto from 'crypto'
import * as utils from './cacheUtils'
import { CompressionMethod } from './constants'
import {
InternalCacheOptions ,
ITypedResponseWithError ,
2024-04-11 05:42:08 +00:00
InternalS3CompletedPart
2023-11-17 06:45:51 +00:00
} from './contracts'
2024-04-10 04:44:06 +00:00
import {
downloadCacheMultiConnection ,
2024-04-15 08:35:22 +00:00
downloadCacheMultipartGCP ,
2024-04-10 04:44:06 +00:00
downloadCacheStreamingGCP
} from './downloadUtils'
2023-11-28 10:15:27 +00:00
import { isSuccessStatusCode , retryTypedResponse } from './requestUtils'
2024-04-11 05:42:08 +00:00
import { Storage } from '@google-cloud/storage'
import {
CommonsCommitCacheRequest ,
CommonsCommitCacheResponse ,
2024-04-15 04:43:42 +00:00
CommonsDeleteCacheResponse ,
2024-04-11 05:42:08 +00:00
CommonsGetCacheResponse ,
CommonsReserveCacheRequest ,
CommonsReserveCacheResponse
} from './warpcache-ts-sdk'
import { multiPartUploadToGCS , uploadFileToS3 } from './uploadUtils'
2024-04-15 04:43:42 +00:00
import { CommonsGetCacheRequest } from './warpcache-ts-sdk/models/commons-get-cache-request'
import { CommonsDeleteCacheRequest } from './warpcache-ts-sdk/models/commons-delete-cache-request'
2024-04-15 06:28:21 +00:00
import { OAuth2Client } from 'google-auth-library'
2023-11-17 06:45:51 +00:00
const versionSalt = '1.0'
function getCacheApiUrl ( resource : string ) : string {
2023-11-22 12:12:10 +00:00
const baseUrl : string =
2024-04-10 04:44:06 +00:00
process . env [ 'WARPBUILD_CACHE_URL' ] ? ? 'https://cache.warpbuild.com'
2023-11-17 06:45:51 +00:00
if ( ! baseUrl ) {
throw new Error ( 'Cache Service Url not found, unable to restore cache.' )
}
2024-04-11 05:42:08 +00:00
const url = ` ${ baseUrl } /v1/ ${ resource } `
2023-11-17 06:45:51 +00:00
core . debug ( ` Resource Url: ${ url } ` )
return url
}
function createAcceptHeader ( type : string , apiVersion : string ) : string {
return ` ${ type } ;api-version= ${ apiVersion } `
}
2024-04-15 04:43:42 +00:00
function getVCSRepository ( ) : string {
const vcsRepository = process . env [ 'GITHUB_REPOSITORY' ] ? ? ''
return vcsRepository
}
function getVCSRef ( ) : string {
const vcsBranch = process . env [ 'GITHUB_REF' ] ? ? ''
return vcsBranch
}
2024-04-15 12:22:36 +00:00
function getAnnotations ( ) : { [ key : string ] : string } {
const annotations : { [ key : string ] : string } = {
GITHUB_WORKFLOW : process.env [ 'GITHUB_WORKFLOW' ] ? ? '' ,
GITHUB_RUN_ID : process.env [ 'GITHUB_RUN_ID' ] ? ? '' ,
GITHUB_RUN_ATTEMPT : process.env [ 'GITHUB_RUN_ATTEMPT' ] ? ? '' ,
GITHUB_JOB : process.env [ 'GITHUB_JOB' ] ? ? '' ,
GITHUB_REPOSITORY : process.env [ 'GITHUB_REPOSITORY' ] ? ? '' ,
GITHUB_REF : process.env [ 'GITHUB_REF' ] ? ? '' ,
GITHUB_ACTION : process.env [ 'GITHUB_ACTION' ] ? ? '' ,
RUNNER_NAME : process.env [ 'RUNNER_NAME' ] ? ? ''
}
return annotations
}
2023-11-17 06:45:51 +00:00
function getRequestOptions ( ) : RequestOptions {
const requestOptions : RequestOptions = {
headers : {
Accept : createAcceptHeader ( 'application/json' , 'v1' )
}
}
return requestOptions
}
function createHttpClient ( ) : HttpClient {
2024-04-10 04:44:06 +00:00
const token = process . env [ 'WARPBUILD_RUNNER_VERIFICATION_TOKEN' ] ? ? ''
2023-11-17 06:45:51 +00:00
const bearerCredentialHandler = new BearerCredentialHandler ( token )
return new HttpClient (
2024-04-10 04:44:06 +00:00
'warp/cache' ,
2023-11-17 06:45:51 +00:00
[ bearerCredentialHandler ] ,
getRequestOptions ( )
)
}
export function getCacheVersion (
paths : string [ ] ,
compressionMethod? : CompressionMethod ,
2024-04-15 04:43:42 +00:00
enableCrossOsArchive = false ,
enableCrossArchArchive = false
2023-11-17 06:45:51 +00:00
) : string {
const components = paths
// Add compression method to cache version to restore
// compressed cache as per compression method
if ( compressionMethod ) {
components . push ( compressionMethod )
}
// Only check for windows platforms if enableCrossOsArchive is false
if ( process . platform === 'win32' && ! enableCrossOsArchive ) {
components . push ( 'windows-only' )
}
2024-04-15 04:43:42 +00:00
// Add architecture to cache version
if ( ! enableCrossArchArchive ) {
components . push ( process . arch )
}
2023-11-17 06:45:51 +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 (
2024-04-15 04:43:42 +00:00
key : string ,
restoreKeys : string [ ] ,
2023-11-17 06:45:51 +00:00
paths : string [ ] ,
options? : InternalCacheOptions
2024-04-11 05:42:08 +00:00
) : Promise < CommonsGetCacheResponse | null > {
2023-11-17 06:45:51 +00:00
const httpClient = createHttpClient ( )
const version = getCacheVersion (
paths ,
options ? . compressionMethod ,
2024-04-15 04:43:42 +00:00
options ? . enableCrossOsArchive ,
options ? . enableCrossArchArchive
2023-11-17 06:45:51 +00:00
)
2024-04-15 04:43:42 +00:00
const getCacheRequest : CommonsGetCacheRequest = {
cache_key : key ,
restore_keys : restoreKeys ,
cache_version : version ,
vcs_repository : getVCSRepository ( ) ,
2024-04-15 12:22:36 +00:00
vcs_ref : getVCSRef ( ) ,
annotations : getAnnotations ( )
2024-04-15 04:43:42 +00:00
}
2023-11-17 06:45:51 +00:00
const response = await retryTypedResponse ( 'getCacheEntry' , async ( ) = >
2024-04-15 04:43:42 +00:00
httpClient . postJson < CommonsGetCacheResponse > (
getCacheApiUrl ( 'cache/get' ) ,
getCacheRequest
)
2023-11-17 06:45:51 +00:00
)
2024-04-10 04:44:06 +00:00
2023-11-17 06:45:51 +00:00
if ( response . statusCode === 204 ) {
2024-04-11 05:42:08 +00:00
// TODO: List cache for primary key only if cache miss occurs
// if (core.isDebug()) {
// await printCachesListForDiagnostics(keys[0], httpClient, version)
// }
2023-11-17 06:45:51 +00:00
return null
}
if ( ! isSuccessStatusCode ( response . statusCode ) ) {
throw new Error ( ` Cache service responded with ${ response . statusCode } ` )
}
const cacheResult = response . result
core . debug ( ` Cache Result: ` )
core . debug ( JSON . stringify ( cacheResult ) )
return cacheResult
}
2024-04-11 05:42:08 +00:00
/ *
2023-11-17 06:45:51 +00:00
async function printCachesListForDiagnostics (
key : string ,
httpClient : HttpClient ,
version : string
) : Promise < void > {
const resource = ` caches?key= ${ encodeURIComponent ( key ) } `
const response = await retryTypedResponse ( 'listCache' , async ( ) = >
httpClient . getJson < ArtifactCacheList > ( getCacheApiUrl ( resource ) )
)
if ( response . statusCode === 200 ) {
const cacheListResult = response . result
const totalCount = cacheListResult ? . totalCount
if ( totalCount && totalCount > 0 ) {
core . debug (
` 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: `
)
2023-11-22 12:12:10 +00:00
for ( const cacheEntry of cacheListResult ? . artifactCaches ? ? [ ] ) {
2023-11-17 06:45:51 +00:00
core . debug (
2023-11-22 12:12:10 +00:00
` Cache Key: ${ cacheEntry ? . cache_key } , Cache Version: ${ cacheEntry ? . cache_version } `
2023-11-17 06:45:51 +00:00
)
}
}
}
}
2024-04-11 05:42:08 +00:00
* /
2023-11-17 06:45:51 +00:00
export async function downloadCache (
2024-04-15 08:35:22 +00:00
provider : string ,
2023-11-17 06:45:51 +00:00
archiveLocation : string ,
2024-04-15 08:35:22 +00:00
archivePath : string ,
gcsToken? : string
2023-11-17 06:45:51 +00:00
) : Promise < void > {
2024-04-15 08:35:22 +00:00
switch ( provider ) {
case 's3' :
await downloadCacheMultiConnection ( archiveLocation , archivePath , 8 )
break
case 'gcs' : {
if ( ! gcsToken ) {
throw new Error (
'Unable to download cache from GCS. GCP token is not provided.'
)
}
const oauth2Client = new OAuth2Client ( )
oauth2Client . setCredentials ( { access_token : gcsToken } )
const storage = new Storage ( {
authClient : oauth2Client
} )
await downloadCacheMultipartGCP ( storage , archiveLocation , archivePath )
break
}
}
2023-11-17 06:45:51 +00:00
}
2024-04-10 04:44:06 +00:00
export function downloadCacheStreaming (
provider : string ,
2024-04-11 05:42:08 +00:00
archiveLocation : string ,
gcsToken? : string
2024-04-10 04:44:06 +00:00
) : NodeJS . ReadableStream | undefined {
switch ( provider ) {
case 's3' :
return undefined
2024-04-11 05:42:08 +00:00
case 'gcs' : {
if ( ! gcsToken ) {
throw new Error (
'Unable to download cache from GCS. GCP token is not provided.'
)
}
2024-04-15 06:28:21 +00:00
const oauth2Client = new OAuth2Client ( )
oauth2Client . setCredentials ( { access_token : gcsToken } )
2024-04-11 05:42:08 +00:00
const storage = new Storage ( {
2024-04-15 06:28:21 +00:00
authClient : oauth2Client
2024-04-11 05:42:08 +00:00
} )
return downloadCacheStreamingGCP ( storage , archiveLocation )
}
2024-04-10 04:44:06 +00:00
default :
return undefined
}
}
2023-11-17 06:45:51 +00:00
export async function reserveCache (
2023-11-22 12:12:10 +00:00
cacheKey : string ,
numberOfChunks : number ,
2024-04-15 04:43:42 +00:00
cacheVersion : string
2024-04-11 05:42:08 +00:00
) : Promise < ITypedResponseWithError < CommonsReserveCacheResponse > > {
2023-11-17 06:45:51 +00:00
const httpClient = createHttpClient ( )
2024-04-11 05:42:08 +00:00
const reserveCacheRequest : CommonsReserveCacheRequest = {
2023-11-22 12:12:10 +00:00
cache_key : cacheKey ,
2024-04-15 04:43:42 +00:00
cache_version : cacheVersion ,
2023-11-22 12:12:10 +00:00
number_of_chunks : numberOfChunks ,
2024-04-15 04:43:42 +00:00
content_type : 'application/zstd' ,
vcs_repository : getVCSRepository ( ) ,
2024-04-15 12:22:36 +00:00
vcs_ref : getVCSRef ( ) ,
annotations : getAnnotations ( )
2023-11-17 06:45:51 +00:00
}
const response = await retryTypedResponse ( 'reserveCache' , async ( ) = >
2024-04-11 05:42:08 +00:00
httpClient . postJson < CommonsReserveCacheResponse > (
2023-11-22 12:12:10 +00:00
getCacheApiUrl ( 'cache/reserve' ) ,
2023-11-17 06:45:51 +00:00
reserveCacheRequest
)
)
return response
}
async function commitCache (
2023-11-22 12:12:10 +00:00
cacheKey : string ,
cacheVersion : string ,
2024-04-11 05:42:08 +00:00
uploadKey? : string ,
uploadID? : string ,
parts? : InternalS3CompletedPart [ ]
) : Promise < TypedResponse < CommonsCommitCacheResponse > > {
const httpClient = createHttpClient ( )
if ( ! parts ) {
parts = [ ]
}
const commitCacheRequest : CommonsCommitCacheRequest = {
2023-11-22 12:12:10 +00:00
cache_key : cacheKey ,
cache_version : cacheVersion ,
upload_key : uploadKey ,
upload_id : uploadID ,
parts : parts ,
2024-04-15 04:43:42 +00:00
vcs_type : 'github' ,
vcs_repository : getVCSRepository ( ) ,
2024-04-15 12:22:36 +00:00
vcs_ref : getVCSRef ( ) ,
annotations : getAnnotations ( )
2023-11-22 12:12:10 +00:00
}
2023-11-17 06:45:51 +00:00
return await retryTypedResponse ( 'commitCache' , async ( ) = >
2024-04-11 05:42:08 +00:00
httpClient . postJson < CommonsCommitCacheResponse > (
2023-11-22 12:12:10 +00:00
getCacheApiUrl ( ` cache/commit ` ) ,
2023-11-17 06:45:51 +00:00
commitCacheRequest
)
)
}
export async function saveCache (
2024-04-11 05:42:08 +00:00
provider : string ,
2023-11-22 12:12:10 +00:00
cacheKey : string ,
cacheVersion : string ,
2024-04-11 05:42:08 +00:00
archivePath : string ,
S3UploadId? : string ,
S3UploadKey? : string ,
S3NumberOfChunks? : number ,
S3PreSignedURLs? : string [ ] ,
GCSAuthToken? : string ,
GCSBucketName? : string ,
GCSObjectName? : string
2023-11-22 12:12:10 +00:00
) : Promise < string > {
2024-04-11 05:42:08 +00:00
const cacheSize = utils . getArchiveFileSizeInBytes ( archivePath )
core . info (
` Cache Size: ~ ${ Math . round ( cacheSize / ( 1024 * 1024 ) ) } MB ( ${ cacheSize } B) `
)
let commitCacheResponse : TypedResponse < CommonsCommitCacheResponse > = {
headers : { } ,
statusCode : 0 ,
result : null
2023-11-22 12:12:10 +00:00
}
2024-04-11 05:42:08 +00:00
let cacheKeyResponse = ''
2023-11-17 06:45:51 +00:00
2024-04-11 05:42:08 +00:00
switch ( provider ) {
case 's3' : {
if (
! S3NumberOfChunks ||
! S3PreSignedURLs ||
! S3UploadId ||
! S3UploadKey
) {
throw new Error (
'Unable to upload cache to S3. One of the following required parameters is missing: numberOfChunks, preSignedURLs, uploadId, uploadKey.'
)
}
2023-11-22 12:12:10 +00:00
2024-04-11 05:42:08 +00:00
// Number of chunks should match the number of pre-signed URLs
if ( S3NumberOfChunks !== S3PreSignedURLs . length ) {
throw new Error (
` Number of chunks ( ${ S3NumberOfChunks } ) should match the number of pre-signed URLs ( ${ S3PreSignedURLs . length } ). `
)
}
2023-11-17 06:45:51 +00:00
2024-04-11 05:42:08 +00:00
core . debug ( 'Uploading cache' )
const completedParts = await uploadFileToS3 ( S3PreSignedURLs , archivePath )
// Sort parts in ascending order by partNumber
completedParts . sort ( ( a , b ) = > a . PartNumber - b . PartNumber )
core . debug ( 'Committing cache' )
commitCacheResponse = await commitCache (
cacheKey ,
cacheVersion ,
S3UploadKey ,
S3UploadId ,
completedParts
)
cacheKeyResponse = commitCacheResponse . result ? . s3 ? . cache_key ? ? ''
break
}
case 'gcs' : {
if ( ! GCSBucketName || ! GCSObjectName || ! GCSAuthToken ) {
throw new Error (
'Unable to upload cache to GCS. One of the following required parameters is missing: GCSBucketName, GCSObjectName, GCSAuthToken.'
)
}
core . debug ( 'Uploading cache' )
2024-04-15 06:28:21 +00:00
const oauth2Client = new OAuth2Client ( )
oauth2Client . setCredentials ( { access_token : GCSAuthToken } )
2024-04-11 05:42:08 +00:00
const storage = new Storage ( {
2024-04-15 06:28:21 +00:00
authClient : oauth2Client
2024-04-11 05:42:08 +00:00
} )
await multiPartUploadToGCS (
storage ,
archivePath ,
GCSBucketName ,
GCSObjectName
)
core . debug ( 'Committing cache' )
commitCacheResponse = await commitCache ( cacheKey , cacheVersion )
cacheKeyResponse = commitCacheResponse . result ? . gcs ? . cache_key ? ? ''
break
}
}
2023-11-17 06:45:51 +00:00
if ( ! isSuccessStatusCode ( commitCacheResponse . statusCode ) ) {
throw new Error (
` Cache service responded with ${ commitCacheResponse . statusCode } during commit cache. `
)
}
core . info ( 'Cache saved successfully' )
2024-04-11 05:42:08 +00:00
return cacheKeyResponse
2023-11-22 12:12:10 +00:00
}
2024-04-15 04:43:42 +00:00
export async function deleteCache ( cacheKey : string , cacheVersion : string ) {
2023-11-22 12:12:10 +00:00
const httpClient = createHttpClient ( )
2024-04-15 04:43:42 +00:00
const deleteCacheRequest : CommonsDeleteCacheRequest = {
cache_key : cacheKey ,
cache_version : cacheVersion ,
vcs_repository : getVCSRepository ( ) ,
2024-04-15 12:22:36 +00:00
vcs_ref : getVCSRef ( ) ,
annotations : getAnnotations ( )
2024-04-15 04:43:42 +00:00
}
const response = await retryTypedResponse ( 'deleteCacheEntry' , async ( ) = >
httpClient . postJson < CommonsDeleteCacheResponse > (
getCacheApiUrl ( 'cache/delete' ) ,
deleteCacheRequest
2023-11-22 12:12:10 +00:00
)
2024-04-15 04:43:42 +00:00
)
if ( ! isSuccessStatusCode ( response . statusCode ) ) {
throw new Error ( ` Cache service responded with ${ response . statusCode } ` )
2023-11-22 12:12:10 +00:00
}
2023-11-17 06:45:51 +00:00
}