From 69c281997208b0651c678dde4834d8aaf34b6ccb Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Tue, 17 Feb 2026 13:38:00 +0000 Subject: [PATCH] Add save and restore methods --- src/overlay/status.ts | 118 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/src/overlay/status.ts b/src/overlay/status.ts index 4695dce70..4a61936fe 100644 --- a/src/overlay/status.ts +++ b/src/overlay/status.ts @@ -8,8 +8,124 @@ * We use the Actions cache as a lightweight way of providing this functionality. */ +import * as fs from "fs"; +import * as path from "path"; + +import * as actionsCache from "@actions/cache"; + +import { getTemporaryDirectory } from "../actions-util"; import { type CodeQL } from "../codeql"; -import { DiskUsage } from "../util"; +import { Logger } from "../logging"; +import { + DiskUsage, + getErrorMessage, + waitForResultWithTimeLimit, +} from "../util"; + +/** The maximum time to wait for a cache operation to complete. */ +const MAX_CACHE_OPERATION_MS = 30_000; + +/** File name for the serialized overlay status. */ +const STATUS_FILE_NAME = "overlay-status.json"; + +/** Status of an overlay analysis for a particular language. */ +export interface OverlayStatus { + /** Whether the job successfully built an overlay base database. */ + builtOverlayBaseDatabase: boolean; +} + +/** + * Retrieve overlay status from the Actions cache, if available. + * + * @returns `undefined` if no status was found in the cache (e.g. first run with + * this cache key) or if the cache operation fails. + */ +export async function getOverlayStatus( + codeql: CodeQL, + language: string, + diskUsage: DiskUsage, + logger: Logger, +): Promise { + const cacheKey = await getCacheKey(codeql, language, diskUsage); + const statusFile = path.join( + getTemporaryDirectory(), + "overlay-status", + language, + STATUS_FILE_NAME, + ); + await fs.promises.mkdir(path.dirname(statusFile), { recursive: true }); + + try { + const foundKey = await waitForResultWithTimeLimit( + MAX_CACHE_OPERATION_MS, + actionsCache.restoreCache([statusFile], cacheKey), + () => { + logger.info("Timed out restoring overlay status from cache"); + }, + ); + if (foundKey === undefined) { + logger.debug("No overlay status found in Actions cache"); + return undefined; + } + + if (!fs.existsSync(statusFile)) { + logger.debug( + "Overlay status cache entry found but status file is missing", + ); + return undefined; + } + + const contents = await fs.promises.readFile(statusFile, "utf-8"); + return JSON.parse(contents) as OverlayStatus; + } catch (error) { + logger.warning( + `Failed to restore overlay status from cache: ${getErrorMessage(error)}`, + ); + return undefined; + } +} + +/** + * Save overlay status to the Actions cache. + * + * @returns `true` if the status was saved successfully, `false` otherwise. + */ +export async function saveOverlayStatus( + codeql: CodeQL, + language: string, + diskUsage: DiskUsage, + status: OverlayStatus, + logger: Logger, +): Promise { + const cacheKey = await getCacheKey(codeql, language, diskUsage); + const statusFile = path.join( + getTemporaryDirectory(), + "overlay-status", + language, + STATUS_FILE_NAME, + ); + await fs.promises.mkdir(path.dirname(statusFile), { recursive: true }); + await fs.promises.writeFile(statusFile, JSON.stringify(status)); + + try { + const cacheId = await waitForResultWithTimeLimit( + MAX_CACHE_OPERATION_MS, + actionsCache.saveCache([statusFile], cacheKey), + () => {}, + ); + if (cacheId === undefined) { + logger.warning("Timed out saving overlay status to cache"); + return false; + } + logger.info(`Saved overlay status to Actions cache with key ${cacheKey}`); + return true; + } catch (error) { + logger.warning( + `Failed to save overlay status to cache: ${getErrorMessage(error)}`, + ); + return false; + } +} export async function getCacheKey( codeql: CodeQL,