You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cache/src/utils/gcsCache.ts

276 lines
7.9 KiB
TypeScript

import * as cache from "@actions/cache";
import * as utils from "@actions/cache/lib/internal/cacheUtils";
import { CompressionMethod } from "@actions/cache/lib/internal/constants";
import {
createTar,
extractTar,
listTar
} from "@actions/cache/lib/internal/tar";
import { DownloadOptions, UploadOptions } from "@actions/cache/lib/options";
import * as core from "@actions/core";
import { Storage } from "@google-cloud/storage";
import * as path from "path";
import { Inputs } from "../constants";
import { isGCSAvailable } from "./actionUtils";
const DEFAULT_PATH_PREFIX = "github-cache";
// Function to initialize GCS client using Application Default Credentials
function getGCSClient(): Storage | null {
try {
core.info("Initializing GCS client");
return new Storage();
} catch (error) {
core.warning(
`Failed to initialize GCS client: ${(error as Error).message}`
);
return null;
}
}
export async function restoreCache(
paths: string[],
primaryKey: string,
restoreKeys?: string[],
options?: DownloadOptions,
enableCrossOsArchive?: boolean
): Promise<string | undefined> {
// Check if GCS is available
if (isGCSAvailable()) {
try {
const result = await restoreFromGCS(
paths,
primaryKey,
restoreKeys,
options
);
if (result) {
core.info(`Cache restored from GCS with key: ${result}`);
return result;
}
core.info("Cache not found in GCS, falling back to GitHub cache");
} catch (error) {
core.warning(
`Failed to restore from GCS: ${(error as Error).message}`
);
core.info("Falling back to GitHub cache");
}
} else {
core.info("GCS not configured, using GitHub cache");
}
// Fall back to GitHub cache
return await cache.restoreCache(
paths,
primaryKey,
restoreKeys,
options,
enableCrossOsArchive
);
}
export async function saveCache(
paths: string[],
key: string,
options?: UploadOptions,
enableCrossOsArchive?: boolean
): Promise<number> {
if (isGCSAvailable()) {
try {
const result = await saveToGCS(paths, key);
if (result) {
core.info(`Cache saved to GCS with key: [${key} | ${result}]`);
return 1; // Success ID
}
core.warning("Failed to save to GCS, falling back to GitHub cache");
return -1;
} catch (error) {
core.warning(`Failed to save to GCS: ${(error as Error).message}`);
core.info("Falling back to GitHub cache");
}
} else {
core.info("GCS not configured, using GitHub cache");
}
// Fall back to GitHub cache
return await cache.saveCache(paths, key, options, enableCrossOsArchive);
}
// Function that checks if the cache feature is available (either GCS or GitHub cache)
export function isFeatureAvailable(): boolean {
return isGCSAvailable() || cache.isFeatureAvailable();
}
async function restoreFromGCS(
_paths: string[], // validate paths?
primaryKey: string,
restoreKeys: string[] = [],
options?: DownloadOptions
): Promise<string | undefined> {
const storage = getGCSClient();
if (!storage) {
return undefined;
}
const bucket = core.getInput(Inputs.GCSBucket);
const pathPrefix =
core.getInput(Inputs.GCSPathPrefix) || DEFAULT_PATH_PREFIX;
const compressionMethod = await utils.getCompressionMethod();
const archiveFolder = await utils.createTempDirectory();
const archivePath = path.join(
archiveFolder,
utils.getCacheFileName(compressionMethod)
);
const keys = [primaryKey, ...restoreKeys];
const gcsPath = await findFileOnGCS(
storage,
bucket,
pathPrefix,
keys,
compressionMethod
);
if (!gcsPath) {
core.info(`No matching cache found`);
return undefined;
}
// If lookup only, just return the key
if (options?.lookupOnly) {
core.info(`Cache found in GCS with key: ${gcsPath}`);
return gcsPath;
}
try {
core.info(`Downloading from GCS: ${bucket}/${gcsPath}`);
const file = storage.bucket(bucket).file(gcsPath);
await file.download({ destination: archivePath });
if (core.isDebug()) {
await listTar(archivePath, compressionMethod);
}
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath);
core.info(
`Cache Size: ~${Math.round(
archiveFileSize / (1024 * 1024)
)} MB (${archiveFileSize} B)`
);
await extractTar(archivePath, compressionMethod);
core.info("Cache restored successfully");
return gcsPath;
} catch (error) {
core.warning(`Failed to restore: ${(error as Error).message}`);
} finally {
try {
await utils.unlinkFile(archivePath);
} catch (error) {
core.debug(`Failed to delete archive: ${error}`);
}
}
}
function getGCSPath(
pathPrefix: string,
key: string,
compressionMethod: CompressionMethod
): string {
return `${pathPrefix}/${key}.${utils.getCacheFileName(compressionMethod)}`;
}
async function saveToGCS(
paths: string[],
key: string
): Promise<string | undefined> {
const storage = getGCSClient();
if (!storage) {
return undefined;
}
const bucket = core.getInput(Inputs.GCSBucket);
const pathPrefix =
core.getInput(Inputs.GCSPathPrefix) || DEFAULT_PATH_PREFIX;
const compressionMethod = await utils.getCompressionMethod();
const cachePaths = await utils.resolvePaths(paths);
core.debug("Cache Paths:");
core.debug(`${JSON.stringify(cachePaths)}`);
if (cachePaths.length === 0) {
throw new Error(
`Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.`
);
}
const archiveFolder = await utils.createTempDirectory();
const archivePath = path.join(
archiveFolder,
utils.getCacheFileName(compressionMethod)
);
core.debug(`Archive Path: ${archivePath}`);
try {
await createTar(archiveFolder, cachePaths, compressionMethod);
if (core.isDebug()) {
await listTar(archivePath, compressionMethod);
}
const gcsPath = getGCSPath(pathPrefix, key, compressionMethod);
core.info(`Uploading to GCS: ${bucket}/${gcsPath}`);
const [file] = await storage.bucket(bucket).upload(archivePath, {
destination: gcsPath,
resumable: false
});
return file.metadata.id;
} catch (error) {
core.warning(
`Error creating or uploading cache: ${(error as Error).message}`
);
throw new Error(
`Error creating or uploading cache: ${(error as Error).message}`
);
} finally {
try {
await utils.unlinkFile(archivePath);
} catch (error) {
core.debug(`Failed to delete archive: ${error}`);
}
}
}
async function findFileOnGCS(
storage: Storage,
bucket: string,
pathPrefix: string,
keys: string[],
compressionMethod: CompressionMethod
): Promise<string | undefined> {
for (const key of keys) {
const gcsPath = getGCSPath(pathPrefix, key, compressionMethod);
if (await checkFileExists(storage, bucket, gcsPath)) {
core.info(`Found file on bucket: ${bucket} with key: ${gcsPath}`);
return gcsPath;
}
}
return undefined;
}
async function checkFileExists(
storage: Storage,
bucket: string,
path: string
): Promise<boolean> {
const [exists] = await storage.bucket(bucket).file(path).exists();
return exists;
}