2020-05-06 11:10:18 -04:00
import * as core from '@actions/core'
2020-05-06 17:53:22 -04:00
import * as path from 'path'
2020-05-06 11:10:18 -04:00
import * as utils from './internal/cacheUtils'
2024-09-24 03:29:14 -07:00
import * as config from './internal/config'
2020-05-06 11:10:18 -04:00
import * as cacheHttpClient from './internal/cacheHttpClient'
2024-05-29 08:31:54 -07:00
import * as cacheTwirpClient from './internal/cacheTwirpClient'
2024-09-24 03:17:44 -07:00
import { createTar , extractTar , listTar } from './internal/tar'
import { DownloadOptions , UploadOptions } from './options'
2024-06-17 01:17:10 -07:00
import {
2024-09-24 03:17:44 -07:00
CreateCacheEntryRequest ,
CreateCacheEntryResponse ,
FinalizeCacheEntryUploadRequest ,
FinalizeCacheEntryUploadResponse ,
GetCacheEntryDownloadURLRequest ,
GetCacheEntryDownloadURLResponse
} from './generated/results/api/v1/cache'
import { UploadCacheStream } from './internal/v2/upload-cache'
import { StreamExtract } from './internal/v2/download-cache'
2024-06-13 03:16:59 -07:00
import {
UploadZipSpecification ,
getUploadZipSpecification
} from '@actions/artifact/lib/internal/upload/upload-zip-specification'
2024-09-24 03:17:44 -07:00
import { createZipUploadStream } from '@actions/artifact/lib/internal/upload/zip'
import { getBackendIdsFromToken , BackendIds } from '@actions/artifact/lib/internal/shared/util'
2020-05-06 11:10:18 -04:00
2020-05-12 14:47:31 -04:00
export class ValidationError extends Error {
constructor ( message : string ) {
super ( message )
this . name = 'ValidationError'
2020-05-15 12:18:50 -04:00
Object . setPrototypeOf ( this , ValidationError . prototype )
2020-05-12 14:47:31 -04:00
}
}
export class ReserveCacheError extends Error {
constructor ( message : string ) {
super ( message )
this . name = 'ReserveCacheError'
2020-05-15 12:18:50 -04:00
Object . setPrototypeOf ( this , ReserveCacheError . prototype )
2020-05-12 14:47:31 -04:00
}
}
2020-05-06 17:53:22 -04:00
function checkPaths ( paths : string [ ] ) : void {
if ( ! paths || paths . length === 0 ) {
2020-05-12 14:47:31 -04:00
throw new ValidationError (
2020-05-06 17:53:22 -04:00
` Path Validation Error: At least one directory or file path is required `
)
}
}
function checkKey ( key : string ) : void {
if ( key . length > 512 ) {
2020-05-12 14:47:31 -04:00
throw new ValidationError (
2020-05-06 17:53:22 -04:00
` Key Validation Error: ${ key } cannot be larger than 512 characters. `
)
}
const regex = /^[^,]*$/
if ( ! regex . test ( key ) ) {
2020-05-12 14:47:31 -04:00
throw new ValidationError (
` Key Validation Error: ${ key } cannot contain commas. `
)
2020-05-06 17:53:22 -04:00
}
}
2022-03-29 22:19:10 +00:00
/**
* isFeatureAvailable to check the presence of Actions cache service
*
* @returns boolean return true if Actions cache service feature is available, otherwise false
*/
2024-10-09 04:32:57 -07:00
export function isFeatureAvailable ( ) : boolean {
return ! ! config . getCacheServiceVersion
}
2022-03-29 22:19:10 +00:00
2020-05-06 11:10:18 -04:00
/**
* Restores cache from keys
*
2020-05-06 17:53:22 -04:00
* @param paths a list of file paths to restore from the cache
2020-05-06 11:10:18 -04:00
* @param primaryKey an explicit key for restoring the cache
* @param restoreKeys an optional ordered list of keys to use for restoring the cache if no cache hit occurred for key
2020-07-10 10:09:32 -05:00
* @param downloadOptions cache download options
2023-01-04 12:16:25 +05:30
* @param enableCrossOsArchive an optional boolean enabled to restore on windows any cache created on any platform
2020-05-15 12:18:50 -04:00
* @returns string returns the key for the cache hit, otherwise returns undefined
2020-05-06 11:10:18 -04:00
*/
export async function restoreCache (
2020-05-06 17:53:22 -04:00
paths : string [ ] ,
2020-05-06 11:10:18 -04:00
primaryKey : string ,
2020-07-10 10:09:32 -05:00
restoreKeys? : string [ ] ,
2023-01-04 12:16:25 +05:30
options? : DownloadOptions ,
enableCrossOsArchive = false
2020-05-06 11:10:18 -04:00
) : Promise < string | undefined > {
2020-05-06 17:53:22 -04:00
checkPaths ( paths )
2020-05-06 11:10:18 -04:00
2024-09-24 03:29:14 -07:00
const cacheServiceVersion : string = config . getCacheServiceVersion ( )
2024-10-09 04:32:57 -07:00
console . debug ( ` Cache service version: ${ cacheServiceVersion } ` )
2024-09-24 03:29:14 -07:00
switch ( cacheServiceVersion ) {
2024-06-17 01:17:10 -07:00
case "v2" :
return await restoreCachev2 ( paths , primaryKey , restoreKeys , options , enableCrossOsArchive )
case "v1" :
default :
return await restoreCachev1 ( paths , primaryKey , restoreKeys , options , enableCrossOsArchive )
}
}
async function restoreCachev1 (
paths : string [ ] ,
primaryKey : string ,
restoreKeys? : string [ ] ,
options? : DownloadOptions ,
enableCrossOsArchive = false
) {
2020-05-06 17:53:22 -04:00
restoreKeys = restoreKeys || [ ]
const keys = [ primaryKey , . . . restoreKeys ]
2020-05-06 11:10:18 -04:00
2020-05-06 17:53:22 -04:00
core . debug ( 'Resolved Keys:' )
core . debug ( JSON . stringify ( keys ) )
2020-05-06 11:10:18 -04:00
2020-05-06 17:53:22 -04:00
if ( keys . length > 10 ) {
2020-05-12 14:47:31 -04:00
throw new ValidationError (
2020-05-06 17:53:22 -04:00
` Key Validation Error: Keys are limited to a maximum of 10. `
)
}
for ( const key of keys ) {
checkKey ( key )
}
2022-12-27 16:00:28 +05:30
const compressionMethod = await utils . getCompressionMethod ( )
2022-06-24 10:46:20 +05:30
let archivePath = ''
try {
// path are needed to compute version
2022-12-27 16:00:28 +05:30
const cacheEntry = await cacheHttpClient . getCacheEntry ( keys , paths , {
2023-01-04 12:16:25 +05:30
compressionMethod ,
enableCrossOsArchive
2022-06-24 10:46:20 +05:30
} )
if ( ! cacheEntry ? . archiveLocation ) {
2022-12-27 16:00:28 +05:30
// Cache not found
return undefined
2022-06-24 10:46:20 +05:30
}
2020-05-06 17:53:22 -04:00
2023-01-17 17:12:42 +01:00
if ( options ? . lookupOnly ) {
core . info ( 'Lookup only - skipping download' )
2022-12-23 12:44:35 +01:00
return cacheEntry . cacheKey
}
2022-06-24 10:46:20 +05:30
archivePath = path . join (
await utils . createTempDirectory ( ) ,
utils . getCacheFileName ( compressionMethod )
)
core . debug ( ` Archive Path: ${ archivePath } ` )
2020-05-06 11:10:18 -04:00
2020-05-06 17:53:22 -04:00
// Download the cache from the cache entry
2020-07-10 10:09:32 -05:00
await cacheHttpClient . downloadCache (
cacheEntry . archiveLocation ,
archivePath ,
options
)
2020-05-06 17:53:22 -04:00
2020-11-26 01:56:57 +03:00
if ( core . isDebug ( ) ) {
await listTar ( archivePath , compressionMethod )
}
2021-05-03 18:09:44 +03:00
const archiveFileSize = utils . getArchiveFileSizeInBytes ( archivePath )
2020-05-06 17:53:22 -04:00
core . info (
` Cache Size: ~ ${ Math . round (
archiveFileSize / ( 1024 * 1024 )
) } MB ( ${ archiveFileSize } B) `
)
2020-05-06 11:10:18 -04:00
2020-05-06 17:53:22 -04:00
await extractTar ( archivePath , compressionMethod )
2020-11-26 01:56:57 +03:00
core . info ( 'Cache restored successfully' )
2022-06-24 10:46:20 +05:30
return cacheEntry . cacheKey
} catch ( error ) {
const typedError = error as Error
if ( typedError . name === ValidationError . name ) {
throw error
} else {
// Supress all non-validation cache related errors because caching should be optional
core . warning ( ` Failed to restore: ${ ( error as Error ) . message } ` )
}
2020-05-06 17:53:22 -04:00
} finally {
// Try to delete the archive to save space
2020-05-06 11:10:18 -04:00
try {
2020-05-06 17:53:22 -04:00
await utils . unlinkFile ( archivePath )
2020-05-06 11:10:18 -04:00
} catch ( error ) {
2020-05-06 17:53:22 -04:00
core . debug ( ` Failed to delete archive: ${ error } ` )
2020-05-06 11:10:18 -04:00
}
}
2020-05-06 17:53:22 -04:00
2022-06-24 10:46:20 +05:30
return undefined
2020-05-06 11:10:18 -04:00
}
2024-10-09 04:32:57 -07:00
/**
* Restores cache using the new Cache Service
*
* @param paths a list of file paths to restore from the cache
* @param primaryKey an explicit key for restoring the cache
* @param restoreKeys an optional ordered list of keys to use for restoring the cache if no cache hit occurred for key
* @param downloadOptions cache download options
* @param enableCrossOsArchive an optional boolean enabled to restore on windows any cache created on any platform
* @returns string returns the key for the cache hit, otherwise returns undefined
*/
2024-06-17 01:17:10 -07:00
async function restoreCachev2 (
paths : string [ ] ,
primaryKey : string ,
restoreKeys? : string [ ] ,
options? : DownloadOptions ,
enableCrossOsArchive = false
) {
restoreKeys = restoreKeys || [ ]
const keys = [ primaryKey , . . . restoreKeys ]
core . debug ( 'Resolved Keys:' )
core . debug ( JSON . stringify ( keys ) )
if ( keys . length > 10 ) {
throw new ValidationError (
` Key Validation Error: Keys are limited to a maximum of 10. `
)
}
for ( const key of keys ) {
checkKey ( key )
}
2024-10-09 04:32:57 -07:00
let archivePath = ''
2024-06-17 01:17:10 -07:00
try {
2024-10-09 04:32:57 -07:00
const twirpClient = cacheTwirpClient . internalCacheTwirpClient ( )
2024-06-24 01:16:11 -07:00
// BackendIds are retrieved form the signed JWT
const backendIds : BackendIds = getBackendIdsFromToken ( )
2024-09-24 03:17:44 -07:00
const compressionMethod = await utils . getCompressionMethod ( )
2024-10-09 04:32:57 -07:00
2024-09-24 03:17:44 -07:00
const request : GetCacheEntryDownloadURLRequest = {
2024-06-24 01:16:11 -07:00
workflowRunBackendId : backendIds.workflowRunBackendId ,
workflowJobRunBackendId : backendIds.workflowJobRunBackendId ,
2024-09-24 03:17:44 -07:00
key : primaryKey ,
restoreKeys : restoreKeys ,
version : utils.getCacheVersion (
paths ,
compressionMethod ,
enableCrossOsArchive ,
) ,
2024-06-17 01:17:10 -07:00
}
2024-10-09 04:32:57 -07:00
2024-09-24 03:17:44 -07:00
const response : GetCacheEntryDownloadURLResponse = await twirpClient . GetCacheEntryDownloadURL ( request )
2024-10-09 04:32:57 -07:00
core . debug ( ` GetCacheEntryDownloadURLResponse: ${ JSON . stringify ( response ) } ` )
2024-06-17 01:17:10 -07:00
2024-09-24 03:17:44 -07:00
if ( ! response . ok ) {
2024-06-17 01:17:10 -07:00
// Cache not found
core . warning ( ` Cache not found for keys: ${ keys . join ( ', ' ) } ` )
return undefined
}
2024-09-24 03:17:44 -07:00
core . info ( ` Cache hit for: ${ request . key } ` )
2024-06-17 02:35:25 -07:00
2024-10-09 04:32:57 -07:00
if ( options ? . lookupOnly ) {
core . info ( 'Lookup only - skipping download' )
return request . key
}
archivePath = path . join (
await utils . createTempDirectory ( ) ,
utils . getCacheFileName ( compressionMethod )
)
core . debug ( ` Archive path: ${ archivePath } ` )
if ( core . isDebug ( ) ) {
await listTar ( archivePath , compressionMethod )
}
core . debug ( ` Starting download of artifact to: ${ archivePath } ` )
const archiveFileSize = utils . getArchiveFileSizeInBytes ( archivePath )
core . info (
` Cache Size: ~ ${ Math . round (
archiveFileSize / ( 1024 * 1024 )
) } MB ( ${ archiveFileSize } B) `
)
// Download the cache from the cache entry
await cacheHttpClient . downloadCache (
response . signedDownloadUrl ,
archivePath ,
options
)
await extractTar ( archivePath , compressionMethod )
core . info ( 'Cache restored successfully' )
return request . key
2024-06-17 01:17:10 -07:00
} catch ( error ) {
throw new Error ( ` Unable to download and extract cache: ${ error . message } ` )
2024-10-09 04:32:57 -07:00
} finally {
try {
await utils . unlinkFile ( archivePath )
} catch ( error ) {
core . debug ( ` Failed to delete archive: ${ error } ` )
}
2024-06-17 01:17:10 -07:00
}
}
2020-05-06 11:10:18 -04:00
/**
2020-05-06 17:53:22 -04:00
* Saves a list of files with the specified key
2020-05-06 11:10:18 -04:00
*
2020-05-06 17:53:22 -04:00
* @param paths a list of file paths to be cached
2020-05-06 11:10:18 -04:00
* @param key an explicit key for restoring the cache
2023-01-04 12:16:25 +05:30
* @param enableCrossOsArchive an optional boolean enabled to save cache on windows which could be restored on any platform
2020-05-12 12:37:03 -04:00
* @param options cache upload options
2020-05-15 12:18:50 -04:00
* @returns number returns cacheId if the cache was saved successfully and throws an error if save fails
2020-05-06 11:10:18 -04:00
*/
2020-05-12 12:37:03 -04:00
export async function saveCache (
paths : string [ ] ,
key : string ,
2023-01-04 12:16:25 +05:30
options? : UploadOptions ,
enableCrossOsArchive = false
2020-05-12 12:37:03 -04:00
) : Promise < number > {
2020-05-06 17:53:22 -04:00
checkPaths ( paths )
checkKey ( key )
2024-09-24 03:17:44 -07:00
2024-09-24 03:29:14 -07:00
const cacheServiceVersion : string = config . getCacheServiceVersion ( )
console . debug ( ` Cache Service Version: ${ cacheServiceVersion } ` )
switch ( cacheServiceVersion ) {
2024-06-13 03:16:59 -07:00
case "v2" :
2024-06-17 02:39:45 -07:00
return await saveCachev2 ( paths , key , options , enableCrossOsArchive )
2024-06-13 03:16:59 -07:00
case "v1" :
default :
2024-06-17 02:39:45 -07:00
return await saveCachev1 ( paths , key , options , enableCrossOsArchive )
2024-05-29 08:31:54 -07:00
}
2024-06-13 03:16:59 -07:00
}
2024-05-29 08:31:54 -07:00
2024-06-13 03:16:59 -07:00
async function saveCachev1 (
paths : string [ ] ,
key : string ,
options? : UploadOptions ,
enableCrossOsArchive = false
) : Promise < number > {
2020-05-06 17:53:22 -04:00
const compressionMethod = await utils . getCompressionMethod ( )
2022-06-24 10:46:20 +05:30
let cacheId = - 1
2020-05-06 17:53:22 -04:00
2020-05-12 14:47:31 -04:00
const cachePaths = await utils . resolvePaths ( paths )
2020-05-06 17:53:22 -04:00
core . debug ( 'Cache Paths:' )
core . debug ( ` ${ JSON . stringify ( cachePaths ) } ` )
2022-05-23 05:59:56 +00:00
if ( cachePaths . length === 0 ) {
2022-05-23 05:39:42 +00:00
throw new Error (
2022-05-23 12:03:18 +00:00
` Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved. `
2022-05-23 05:59:56 +00:00
)
2022-05-22 10:09:13 +00:00
}
2020-05-06 17:53:22 -04:00
const archiveFolder = await utils . createTempDirectory ( )
const archivePath = path . join (
archiveFolder ,
utils . getCacheFileName ( compressionMethod )
)
core . debug ( ` Archive Path: ${ archivePath } ` )
2021-06-28 16:27:09 +01:00
try {
await createTar ( archiveFolder , cachePaths , compressionMethod )
if ( core . isDebug ( ) ) {
await listTar ( archivePath , compressionMethod )
}
2022-04-01 01:41:12 +05:30
const fileSizeLimit = 10 * 1024 * 1024 * 1024 // 10GB per repo limit
2021-06-28 16:27:09 +01:00
const archiveFileSize = utils . getArchiveFileSizeInBytes ( archivePath )
core . debug ( ` File Size: ${ archiveFileSize } ` )
2022-04-04 16:21:58 +05:30
// For GHES, this check will take place in ReserveCache API with enterprise file size limit
if ( archiveFileSize > fileSizeLimit && ! utils . isGhes ( ) ) {
2022-04-01 01:41:12 +05:30
throw new Error (
` Cache size of ~ ${ Math . round (
archiveFileSize / ( 1024 * 1024 )
) } MB ( ${ archiveFileSize } B) is over the 10GB limit, not saving cache. `
)
}
2020-05-06 11:10:18 -04:00
2022-04-04 16:21:58 +05:30
core . debug ( 'Reserving Cache' )
const reserveCacheResponse = await cacheHttpClient . reserveCache (
key ,
paths ,
{
compressionMethod ,
2023-01-04 12:16:25 +05:30
enableCrossOsArchive ,
2022-04-04 16:21:58 +05:30
cacheSize : archiveFileSize
}
)
if ( reserveCacheResponse ? . result ? . cacheId ) {
cacheId = reserveCacheResponse ? . result ? . cacheId
} else if ( reserveCacheResponse ? . statusCode === 400 ) {
2022-04-07 09:08:16 +00:00
throw new Error (
2022-04-04 16:21:58 +05:30
reserveCacheResponse ? . error ? . message ? ?
2024-09-24 03:17:44 -07:00
` Cache size of ~ ${ Math . round (
archiveFileSize / ( 1024 * 1024 )
) } MB ( ${ archiveFileSize } B) is over the data cap limit, not saving cache. `
2022-04-04 16:21:58 +05:30
)
} else {
throw new ReserveCacheError (
` Unable to reserve cache with key ${ key } , another job may be creating this cache. More details: ${ reserveCacheResponse ? . error ? . message } `
)
}
2021-06-28 16:27:09 +01:00
core . debug ( ` Saving Cache (ID: ${ cacheId } ) ` )
await cacheHttpClient . saveCache ( cacheId , archivePath , options )
2022-06-24 10:46:20 +05:30
} catch ( error ) {
const typedError = error as Error
if ( typedError . name === ValidationError . name ) {
throw error
} else if ( typedError . name === ReserveCacheError . name ) {
core . info ( ` Failed to save: ${ typedError . message } ` )
} else {
core . warning ( ` Failed to save: ${ typedError . message } ` )
}
2021-06-28 16:27:09 +01:00
} finally {
// Try to delete the archive to save space
try {
await utils . unlinkFile ( archivePath )
} catch ( error ) {
core . debug ( ` Failed to delete archive: ${ error } ` )
}
}
2020-05-06 11:10:18 -04:00
2020-05-06 17:53:22 -04:00
return cacheId
2020-05-06 11:10:18 -04:00
}
2024-06-13 03:16:59 -07:00
async function saveCachev2 (
paths : string [ ] ,
key : string ,
options? : UploadOptions ,
enableCrossOsArchive = false
) : Promise < number > {
2024-06-24 01:16:11 -07:00
// BackendIds are retrieved form the signed JWT
const backendIds : BackendIds = getBackendIdsFromToken ( )
2024-09-24 03:17:44 -07:00
const compressionMethod = await utils . getCompressionMethod ( )
const version = utils . getCacheVersion (
paths ,
compressionMethod ,
enableCrossOsArchive
)
const twirpClient = cacheTwirpClient . internalCacheTwirpClient ( )
const request : CreateCacheEntryRequest = {
2024-06-24 01:16:11 -07:00
workflowRunBackendId : backendIds.workflowRunBackendId ,
workflowJobRunBackendId : backendIds.workflowJobRunBackendId ,
2024-09-24 03:17:44 -07:00
key : key ,
version : version
2024-06-13 03:16:59 -07:00
}
2024-09-24 03:17:44 -07:00
const response : CreateCacheEntryResponse = await twirpClient . CreateCacheEntry ( request )
core . info ( ` CreateCacheEntryResponse: ${ JSON . stringify ( response ) } ` )
2024-06-13 03:16:59 -07:00
// Archive
// We're going to handle 1 path fow now. This needs to be fixed to handle all
// paths passed in.
const rootDir = path . dirname ( paths [ 0 ] )
const zipSpecs : UploadZipSpecification [ ] = getUploadZipSpecification ( paths , rootDir )
if ( zipSpecs . length === 0 ) {
throw new Error (
` Error with zip specs: ${ zipSpecs . flatMap ( s = > ( s . sourcePath ? [ s . sourcePath ] : [ ] ) ) . join ( ', ' ) } `
)
}
// 0: No compression
// 1: Best speed
// 6: Default compression (same as GNU Gzip)
// 9: Best compression Higher levels will result in better compression, but will take longer to complete. For large files that are not easily compressed, a value of 0 is recommended for significantly faster uploads.
const zipUploadStream = await createZipUploadStream (
zipSpecs ,
6
)
// Cache v2 upload
// inputs:
// - getSignedUploadURL
// - archivePath
core . info ( ` Saving Cache v2: ${ paths [ 0 ] } ` )
2024-09-24 03:17:44 -07:00
await UploadCacheStream ( response . signedUploadUrl , zipUploadStream )
// Finalize the cache entry
const finalizeRequest : FinalizeCacheEntryUploadRequest = {
workflowRunBackendId : backendIds.workflowRunBackendId ,
workflowJobRunBackendId : backendIds.workflowJobRunBackendId ,
key : key ,
version : version ,
sizeBytes : "1024" ,
}
const finalizeResponse : FinalizeCacheEntryUploadResponse = await twirpClient . FinalizeCacheEntryUpload ( finalizeRequest )
core . info ( ` FinalizeCacheEntryUploadResponse: ${ JSON . stringify ( finalizeResponse ) } ` )
2024-06-13 03:16:59 -07:00
return 0
}