import * as core from "@actions/core"; import { CacheSource, Events, Inputs, State } from "./constants"; import { IStateProvider, NullStateProvider, StateProvider } from "./stateProvider"; import * as utils from "./utils/actionUtils"; import * as cache from "./utils/gcsCache"; // Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in // @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to // throw an uncaught exception. Instead of failing this action, just warn. process.on("uncaughtException", e => utils.logWarning(e.message)); export async function saveImpl( stateProvider: IStateProvider ): Promise { let cacheId = -1; try { if (!utils.isCacheFeatureAvailable()) { return; } if (!utils.isValidEvent()) { utils.logWarning( `Event Validation Error: The event type ${ process.env[Events.Key] } is not supported because it's not tied to a branch or tag ref.` ); return; } // If restore has stored a primary key in state, reuse that // Else re-evaluate from inputs const primaryKey = stateProvider.getState(State.CachePrimaryKey) || core.getInput(Inputs.Key); if (!primaryKey) { utils.logWarning(`Key is not specified.`); return; } // If matched restore key is same as primary key, then do not save cache // NO-OP in case of SaveOnly action // // Exception: a hit served by the GitHub fallback while GCS is // configured. GCS is the primary backend, so the entry is written // there too — otherwise the GitHub copy keeps every later job on the // fallback and GCS never gets the key. const restoredKey = stateProvider.getCacheState(); const exactMatch = utils.isExactKeyMatch(primaryKey, restoredKey); const backfillGCS = exactMatch && stateProvider.getState(State.CacheSource) === CacheSource.GitHub && utils.isGCSAvailable(); if (exactMatch && !backfillGCS) { core.info( `Cache hit occurred on the primary key ${primaryKey}, not saving cache.` ); return; } if (backfillGCS) { core.info( `Cache hit on the primary key ${primaryKey} came from the GitHub cache, saving it to GCS.` ); } // Prefer the paths restore recorded: in a nested composite action the // `path` input is empty in the post step (see State.CachePaths). const inputPaths = utils.getInputAsArray(Inputs.Path); const cachePaths = inputPaths.length ? inputPaths : (stateProvider.getState(State.CachePaths) || "") .split("\n") .filter(Boolean); if (cachePaths.length === 0) { utils.logWarning("Input required and not supplied: path"); return; } const enableCrossOsArchive = utils.getInputAsBool( Inputs.EnableCrossOsArchive ); cacheId = await cache.saveCache( cachePaths, primaryKey, { uploadChunkSize: utils.getInputAsInt(Inputs.UploadChunkSize) }, enableCrossOsArchive, !backfillGCS ); if (cacheId != -1) { core.info(`Cache saved with key: ${primaryKey}`); } } catch (error: unknown) { utils.logWarning((error as Error).message); } return cacheId; } export async function saveOnlyRun( earlyExit?: boolean | undefined ): Promise { try { const cacheId = await saveImpl(new NullStateProvider()); if (cacheId === -1) { core.warning(`Cache save failed.`); } } catch (err) { console.error(err); if (earlyExit) { process.exit(1); } } // node will stay alive if any promises are not resolved, // which is a possibility if HTTP requests are dangling // due to retries or timeouts. We know that if we got here // that all promises that we care about have successfully // resolved, so simply exit with success. if (earlyExit) { process.exit(0); } } export async function saveRun(earlyExit?: boolean | undefined): Promise { try { await saveImpl(new StateProvider()); } catch (err) { console.error(err); if (earlyExit) { process.exit(1); } } // node will stay alive if any promises are not resolved, // which is a possibility if HTTP requests are dangling // due to retries or timeouts. We know that if we got here // that all promises that we care about have successfully // resolved, so simply exit with success. if (earlyExit) { process.exit(0); } }