From c020f6afefa4cb3f056012636112aff57a923b97 Mon Sep 17 00:00:00 2001 From: Gary Sassano <10464497+garysassano@users.noreply.github.com> Date: Sun, 14 Jun 2026 00:45:34 +0200 Subject: [PATCH] Add source-keyed target cache --- .github/workflows/target-key.yml | 29 +++++ CHANGELOG.md | 4 + README.md | 16 +++ action.yml | 3 + dist/{cache-Cb-Up9r2.js => cache-BSoAyDaq.js} | 2 +- dist/{cache-1jS6aShy.js => cache-D5WyUDMY.js} | 2 +- ...leanup-ChNUL7jL.js => cleanup-ctNqmXyy.js} | 80 +++++++++++--- dist/restore.js | 77 ++++++++----- dist/save.js | 83 +++++++++----- src/config.ts | 80 +++++++++++--- src/restore.ts | 101 +++++++++++++----- src/save.ts | 84 ++++++++++----- 12 files changed, 420 insertions(+), 141 deletions(-) create mode 100644 .github/workflows/target-key.yml rename dist/{cache-Cb-Up9r2.js => cache-BSoAyDaq.js} (99%) rename dist/{cache-1jS6aShy.js => cache-D5WyUDMY.js} (99%) rename dist/{cleanup-ChNUL7jL.js => cleanup-ctNqmXyy.js} (99%) diff --git a/.github/workflows/target-key.yml b/.github/workflows/target-key.yml new file mode 100644 index 0000000..8b927e1 --- /dev/null +++ b/.github/workflows/target-key.yml @@ -0,0 +1,29 @@ +name: target-key + +on: [push, pull_request] + +permissions: {} + +jobs: + target-key: + if: github.repository == 'Swatinem/rust-cache' + runs-on: ubuntu-latest + + env: + CARGO_TERM_COLOR: always + + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - run: rustup toolchain install stable --profile minimal --no-self-update + + - uses: ./ + with: + workspaces: tests + cache-workspace-crates: "true" + target-key: ${{ hashFiles('tests/**/*.rs', 'tests/Cargo.toml', 'tests/Cargo.lock') }} + + - run: cargo build --release + working-directory: tests diff --git a/CHANGELOG.md b/CHANGELOG.md index d45b75f..4850684 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Add `target-key` for opt-in source-keyed workspace target caching. + ## 2.9.1 - Fix regression in hash calculation diff --git a/README.md b/README.md index 9ece287..7eb8c6f 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,14 @@ sensible defaults. # default: "false" cache-workspace-crates: "" + # An additional key for source-keyed target caching. + # When set together with `cache-targets` and `cache-workspace-crates`, + # workspace target directories are cached separately from CARGO_HOME using + # this key. This allows rebuilt workspace artifacts to be saved under a new + # source key while preserving the normal dependency cache behavior. + # default: empty + target-key: "" + # Determines whether the cache should be saved. # If `false`, the cache is only restored. # Useful for jobs where the matrix is additive e.g. additional Cargo features, @@ -173,6 +181,14 @@ to recreate it from the compressed crate archives in `~/.cargo/registry/cache`. The action will try to restore from a previous `Cargo.lock` version as well, so lockfile updates should only re-build changed dependencies. +When `target-key` is set together with `cache-targets` and +`cache-workspace-crates`, workspace target directories are restored after the +normal CARGO_HOME cache and saved as a separate source-keyed cache. This is useful +for workflows that deliberately cache workspace crates and can provide a stable +source fingerprint, for example `${{ hashFiles('src/**', 'Cargo.toml') }}`. +The target cache still restores from older target caches via restore prefixes, +but exact source matches can become Cargo no-ops across repeated runs. + The action invokes `cargo metadata` to determine the current set of dependencies. Additionally, the action automatically works around diff --git a/action.yml b/action.yml index 1481945..651acc6 100644 --- a/action.yml +++ b/action.yml @@ -44,6 +44,9 @@ inputs: description: "Similar to cache-all-crates. If `true` the workspace crates will be cached." required: false default: "false" + target-key: + description: "An additional key for source-keyed target caching. When set together with cache-targets and cache-workspace-crates, workspace target directories are cached separately using this key." + required: false save-if: description: "Determiners whether the cache should be saved. If `false`, the cache is only restored." required: false diff --git a/dist/cache-Cb-Up9r2.js b/dist/cache-BSoAyDaq.js similarity index 99% rename from dist/cache-Cb-Up9r2.js rename to dist/cache-BSoAyDaq.js index e38c644..76c8631 100644 --- a/dist/cache-Cb-Up9r2.js +++ b/dist/cache-BSoAyDaq.js @@ -1,4 +1,4 @@ -import { f as debug, m as getDefaultExportFromCjs, n as mkdirP, l as exec, w as which, o as warning, i as info, H as HttpCodes, p as HttpClientError, q as HttpClient, t as isDebug, u as setSecret, B as BearerCredentialHandler, e as error } from './cleanup-ChNUL7jL.js'; +import { f as debug, m as getDefaultExportFromCjs, n as mkdirP, l as exec, w as which, o as warning, i as info, H as HttpCodes, p as HttpClientError, q as HttpClient, t as isDebug, u as setSecret, B as BearerCredentialHandler, e as error } from './cleanup-ctNqmXyy.js'; import * as path from 'path'; import * as fs from 'fs'; import { writeFileSync, existsSync } from 'fs'; diff --git a/dist/cache-1jS6aShy.js b/dist/cache-D5WyUDMY.js similarity index 99% rename from dist/cache-1jS6aShy.js rename to dist/cache-D5WyUDMY.js index d25548f..f88b373 100644 --- a/dist/cache-1jS6aShy.js +++ b/dist/cache-D5WyUDMY.js @@ -1,4 +1,4 @@ -import { v as commonjsGlobal, x as requireTunnel, m as getDefaultExportFromCjs, y as getAugmentedNamespace } from './cleanup-ChNUL7jL.js'; +import { v as commonjsGlobal, x as requireTunnel, m as getDefaultExportFromCjs, y as getAugmentedNamespace } from './cleanup-ctNqmXyy.js'; import os__default from 'os'; import crypto__default from 'crypto'; import fs__default from 'fs'; diff --git a/dist/cleanup-ChNUL7jL.js b/dist/cleanup-ctNqmXyy.js similarity index 99% rename from dist/cleanup-ChNUL7jL.js rename to dist/cleanup-ctNqmXyy.js index d3a2bfb..ab620e1 100644 --- a/dist/cleanup-ChNUL7jL.js +++ b/dist/cleanup-ctNqmXyy.js @@ -34120,10 +34120,10 @@ async function getCacheProvider() { let cache; switch (cacheProvider) { case "github": - cache = await import('./cache-Cb-Up9r2.js'); + cache = await import('./cache-BSoAyDaq.js'); break; case "warpbuild": - cache = await import('./cache-1jS6aShy.js').then(function (n) { return n.c; }); + cache = await import('./cache-D5WyUDMY.js').then(function (n) { return n.c; }); break; default: throw new Error(`The \`cache-provider\` \`${cacheProvider}\` is not valid.`); @@ -34192,6 +34192,18 @@ class CacheConfig { cacheKey = ""; /** The secondary (restore) key that only contains the prefix and environment */ restoreKey = ""; + /** Whether the primary cache needs saving in the post action */ + cacheNeedsSave = true; + /** Workspace target paths cached separately when `target-key` is used */ + targetCachePaths = []; + /** The source-keyed workspace target cache key */ + targetCacheKey = ""; + /** The source-keyed workspace target restore keys */ + targetRestoreKeys = []; + /** Whether workspace targets are cached separately from CARGO_HOME */ + targetCacheEnabled = false; + /** Whether the workspace target cache needs saving in the post action */ + targetCacheNeedsSave = false; /** Whether to cache CARGO_HOME/.bin */ cacheBin = true; /** The workspace configurations */ @@ -34387,22 +34399,41 @@ class CacheConfig { let lockHash = digest(hasher); key += `-${lockHash}`; } - self.cacheKey = key; - self.cachePaths = [path__default.join(CARGO_HOME, "registry"), path__default.join(CARGO_HOME, "git")]; + const baseCacheKey = key; + const baseRestoreKey = self.restoreKey; + const cargoCachePaths = [path__default.join(CARGO_HOME, "registry"), path__default.join(CARGO_HOME, "git")]; if (self.cacheBin) { - self.cachePaths = [ - path__default.join(CARGO_HOME, "bin"), - path__default.join(CARGO_HOME, ".crates.toml"), - path__default.join(CARGO_HOME, ".crates2.json"), - ...self.cachePaths, - ]; + cargoCachePaths.unshift(path__default.join(CARGO_HOME, "bin"), path__default.join(CARGO_HOME, ".crates.toml"), path__default.join(CARGO_HOME, ".crates2.json")); } const cacheTargets = getInput("cache-targets").toLowerCase() || "true"; - if (cacheTargets === "true") { - self.cachePaths.push(...workspaces.map((ws) => ws.target)); + const targetCachePaths = cacheTargets === "true" ? workspaces.map((ws) => ws.target) : []; + const cacheDirectories = getInput("cache-directories").trim().split(/\s+/).filter(Boolean); + const targetKey = getInput("target-key"); + const workspaceCrates = getInput("cache-workspace-crates").toLowerCase() || "false"; + if (targetKey && cacheTargets !== "true") { + warning("`target-key` is ignored because `cache-targets` is not `true`."); } - const cacheDirectories = getInput("cache-directories"); - for (const dir of cacheDirectories.trim().split(/\s+/).filter(Boolean)) { + if (targetKey && workspaceCrates !== "true") { + warning("`target-key` is ignored because `cache-workspace-crates` is not `true`."); + } + self.targetCacheEnabled = Boolean(targetKey) && cacheTargets === "true" && workspaceCrates === "true"; + if (self.targetCacheEnabled) { + self.cacheKey = baseCacheKey; + self.cachePaths = [...cargoCachePaths, ...cacheDirectories]; + const targetKeyPrefix = `${baseRestoreKey}-target`; + const targetKeyEnvironment = baseCacheKey.slice(baseRestoreKey.length); + self.targetCachePaths = targetCachePaths; + self.targetCacheKey = `${targetKeyPrefix}${targetKeyEnvironment}-${targetKey}`; + self.targetRestoreKeys = uniqInOrder([`${targetKeyPrefix}${targetKeyEnvironment}-`, `${targetKeyPrefix}-`]); + } + else { + self.cacheKey = baseCacheKey; + self.cachePaths = [...cargoCachePaths]; + } + if (!self.targetCacheEnabled && cacheTargets === "true") { + self.cachePaths.push(...targetCachePaths); + } + for (const dir of self.targetCacheEnabled ? [] : cacheDirectories) { self.cachePaths.push(dir); } const bins = await getCargoBins(); @@ -34438,14 +34469,26 @@ class CacheConfig { for (const workspace of this.workspaces) { info(` ${workspace.root}`); } - info(`Cache Paths:`); + info(`${this.targetCacheEnabled ? "Cargo Cache" : "Cache"} Paths:`); for (const path of this.cachePaths) { info(` ${path}`); } - info(`Restore Key:`); + info(`${this.targetCacheEnabled ? "Cargo Restore" : "Restore"} Key:`); info(` ${this.restoreKey}`); - info(`Cache Key:`); + info(`${this.targetCacheEnabled ? "Cargo Cache" : "Cache"} Key:`); info(` ${this.cacheKey}`); + if (this.targetCacheEnabled) { + info(`Target Cache Paths:`); + for (const path of this.targetCachePaths) { + info(` ${path}`); + } + info(`Target Restore Keys:`); + for (const key of this.targetRestoreKeys) { + info(` ${key}`); + } + info(`Target Cache Key:`); + info(` ${this.targetCacheKey}`); + } info(`.. Prefix:`); info(` - ${this.keyPrefix}`); info(`.. Environment considered:`); @@ -34563,6 +34606,9 @@ function sort_and_uniq(a) { return accumulator; }, []); } +function uniqInOrder(a) { + return a.filter((value, index) => a.indexOf(value) === index); +} async function cleanTargetDir(targetDir, packages, checkTimestamp = false) { debug(`cleaning target directory "${targetDir}"`); diff --git a/dist/restore.js b/dist/restore.js index 9410f7b..9050681 100644 --- a/dist/restore.js +++ b/dist/restore.js @@ -1,4 +1,4 @@ -import { e as error, g as getCacheProvider, a as getInput, b as exportVariable, C as CacheConfig, i as info, c as cleanTargetDir, r as reportError, s as setOutput } from './cleanup-ChNUL7jL.js'; +import { e as error, g as getCacheProvider, a as getInput, b as exportVariable, C as CacheConfig, i as info, r as reportError, s as setOutput, c as cleanTargetDir } from './cleanup-ctNqmXyy.js'; import 'os'; import 'crypto'; import 'fs'; @@ -47,44 +47,43 @@ async function run() { return; } try { - var cacheOnFailure = getInput("cache-on-failure").toLowerCase(); + let cacheOnFailure = getInput("cache-on-failure").toLowerCase(); if (cacheOnFailure !== "true") { cacheOnFailure = "false"; } - var lookupOnly = getInput("lookup-only").toLowerCase() === "true"; + const lookupOnly = getInput("lookup-only").toLowerCase() === "true"; exportVariable("CACHE_ON_FAILURE", cacheOnFailure); exportVariable("CARGO_INCREMENTAL", 0); const config = await CacheConfig.new(); config.printInfo(cacheProvider); info(""); info(`... ${lookupOnly ? "Checking" : "Restoring"} cache ...`); - const key = config.cacheKey; - // Pass a copy of cachePaths to avoid mutating the original array as reported by: - // https://github.com/actions/toolkit/pull/1378 - // TODO: remove this once the underlying bug is fixed. - const restoreKey = await cacheProvider.cache.restoreCache(config.cachePaths.slice(), key, [config.restoreKey], { - lookupOnly, - }); - if (restoreKey) { - const match = restoreKey.localeCompare(key, undefined, { - sensitivity: "accent", - }) === 0; - info(`${lookupOnly ? "Found" : "Restored from"} cache key "${restoreKey}" full match: ${match}.`); - if (!match) { - // pre-clean the target directory on cache mismatch - for (const workspace of config.workspaces) { - try { - await cleanTargetDir(workspace.target, [], true); - } - catch { } - } - // We restored the cache but it is not a full match. + const cacheResult = await restoreCache(cacheProvider, config.cachePaths, config.cacheKey, [config.restoreKey], lookupOnly); + config.cacheNeedsSave = !cacheResult.match; + if (config.targetCacheEnabled) { + if (cacheResult.found && !cacheResult.match) { + // pre-clean the target directory on cargo cache mismatch before restoring target cache + await cleanTargets(config); + } + const targetResult = await restoreCache(cacheProvider, config.targetCachePaths, config.targetCacheKey, config.targetRestoreKeys, lookupOnly, "target"); + config.targetCacheNeedsSave = !targetResult.match; + if (targetResult.found && !targetResult.match) { + // pre-clean the target directory on target cache mismatch + await cleanTargets(config); + } + if (!cacheResult.match || !targetResult.match) { config.saveState(); } - setCacheHitOutput(match); + setCacheHitOutput(cacheResult.match && targetResult.match); + } + else if (cacheResult.match) { + setCacheHitOutput(true); } else { - info("No cache found."); + if (cacheResult.found) { + // pre-clean the target directory on cache mismatch + await cleanTargets(config); + } config.saveState(); setCacheHitOutput(false); } @@ -98,4 +97,30 @@ async function run() { function setCacheHitOutput(cacheHit) { setOutput("cache-hit", cacheHit.toString()); } +async function restoreCache(cacheProvider, paths, key, restoreKeys, lookupOnly, name = "") { + const label = name ? `${name} cache` : "cache"; + // Pass a copy of cachePaths to avoid mutating the original array as reported by: + // https://github.com/actions/toolkit/pull/1378 + // TODO: remove this once the underlying bug is fixed. + const restoreKey = await cacheProvider.cache.restoreCache(paths.slice(), key, restoreKeys, { + lookupOnly, + }); + if (!restoreKey) { + info(`No ${label} found.`); + return { found: false, match: false }; + } + const match = restoreKey.localeCompare(key, undefined, { + sensitivity: "accent", + }) === 0; + info(`${lookupOnly ? "Found" : "Restored from"} ${label} key "${restoreKey}" full match: ${match}.`); + return { found: true, match }; +} +async function cleanTargets(config) { + for (const workspace of config.workspaces) { + try { + await cleanTargetDir(workspace.target, [], true); + } + catch { } + } +} run(); diff --git a/dist/save.js b/dist/save.js index 0fc97a1..4eec008 100644 --- a/dist/save.js +++ b/dist/save.js @@ -1,4 +1,4 @@ -import { e as error, g as getCacheProvider, a as getInput, d as isCacheUpToDate, i as info, C as CacheConfig, c as cleanTargetDir, f as debug, h as cleanRegistry, j as cleanBin, k as cleanGit, r as reportError, l as exec } from './cleanup-ChNUL7jL.js'; +import { e as error, g as getCacheProvider, a as getInput, d as isCacheUpToDate, i as info, C as CacheConfig, c as cleanTargetDir, f as debug, h as cleanRegistry, j as cleanBin, k as cleanGit, r as reportError, l as exec } from './cleanup-ctNqmXyy.js'; import 'os'; import 'crypto'; import 'fs'; @@ -58,6 +58,8 @@ async function run() { if (process.env["RUNNER_OS"] == "macOS") { await macOsWorkaround(); } + const cleanCargo = !config.targetCacheEnabled || config.cacheNeedsSave; + const cleanTargets = !config.targetCacheEnabled || config.targetCacheNeedsSave; const workspaceCrates = getInput("cache-workspace-crates").toLowerCase() || "false"; const allPackages = []; for (const workspace of config.workspaces) { @@ -67,43 +69,62 @@ async function run() { packages.push(...wsMembers); } allPackages.push(...packages); + if (cleanTargets) { + try { + info(`... Cleaning ${workspace.target} ...`); + await cleanTargetDir(workspace.target, packages); + } + catch (e) { + debug(`${e.stack}`); + } + } + } + if (cleanCargo) { try { - info(`... Cleaning ${workspace.target} ...`); - await cleanTargetDir(workspace.target, packages); + const crates = getInput("cache-all-crates").toLowerCase() || "false"; + info(`... Cleaning cargo registry (cache-all-crates: ${crates}) ...`); + await cleanRegistry(allPackages, crates !== "true"); + } + catch (e) { + debug(`${e.stack}`); + } + if (config.cacheBin) { + try { + info(`... Cleaning cargo/bin ...`); + await cleanBin(config.cargoBins); + } + catch (e) { + debug(`${e.stack}`); + } + } + try { + info(`... Cleaning cargo git cache ...`); + await cleanGit(allPackages); } catch (e) { debug(`${e.stack}`); } } - try { - const crates = getInput("cache-all-crates").toLowerCase() || "false"; - info(`... Cleaning cargo registry (cache-all-crates: ${crates}) ...`); - await cleanRegistry(allPackages, crates !== "true"); - } - catch (e) { - debug(`${e.stack}`); - } - if (config.cacheBin) { - try { - info(`... Cleaning cargo/bin ...`); - await cleanBin(config.cargoBins); + if (config.targetCacheEnabled) { + if (config.cacheNeedsSave) { + info(`... Saving cargo cache ...`); + await saveCache(cacheProvider, config.cachePaths, config.cacheKey); } - catch (e) { - debug(`${e.stack}`); + else { + info(`Cargo cache up-to-date.`); + } + if (config.targetCacheNeedsSave) { + info(`... Saving target cache ...`); + await saveCache(cacheProvider, config.targetCachePaths, config.targetCacheKey); + } + else { + info(`Target cache up-to-date.`); } } - try { - info(`... Cleaning cargo git cache ...`); - await cleanGit(allPackages); + else { + info(`... Saving cache ...`); + await saveCache(cacheProvider, config.cachePaths, config.cacheKey); } - catch (e) { - debug(`${e.stack}`); - } - info(`... Saving cache ...`); - // Pass a copy of cachePaths to avoid mutating the original array as reported by: - // https://github.com/actions/toolkit/pull/1378 - // TODO: remove this once the underlying bug is fixed. - await cacheProvider.cache.saveCache(config.cachePaths.slice(), config.cacheKey); } catch (e) { reportError(e); @@ -111,6 +132,12 @@ async function run() { process.exit(); } run(); +async function saveCache(cacheProvider, paths, key) { + // Pass a copy of cachePaths to avoid mutating the original array as reported by: + // https://github.com/actions/toolkit/pull/1378 + // TODO: remove this once the underlying bug is fixed. + await cacheProvider.cache.saveCache(paths.slice(), key); +} async function macOsWorkaround() { try { // Workaround for https://github.com/actions/cache/issues/403 diff --git a/src/config.ts b/src/config.ts index 4545dba..fd80b23 100644 --- a/src/config.ts +++ b/src/config.ts @@ -27,6 +27,20 @@ export class CacheConfig { /** The secondary (restore) key that only contains the prefix and environment */ public restoreKey = ""; + /** Whether the primary cache needs saving in the post action */ + public cacheNeedsSave = true; + + /** Workspace target paths cached separately when `target-key` is used */ + public targetCachePaths: Array = []; + /** The source-keyed workspace target cache key */ + public targetCacheKey = ""; + /** The source-keyed workspace target restore keys */ + public targetRestoreKeys: Array = []; + /** Whether workspace targets are cached separately from CARGO_HOME */ + public targetCacheEnabled = false; + /** Whether the workspace target cache needs saving in the post action */ + public targetCacheNeedsSave = false; + /** Whether to cache CARGO_HOME/.bin */ public cacheBin: boolean = true; @@ -266,24 +280,50 @@ export class CacheConfig { key += `-${lockHash}`; } - self.cacheKey = key; + const baseCacheKey = key; + const baseRestoreKey = self.restoreKey; - self.cachePaths = [path.join(CARGO_HOME, "registry"), path.join(CARGO_HOME, "git")]; + const cargoCachePaths = [path.join(CARGO_HOME, "registry"), path.join(CARGO_HOME, "git")]; if (self.cacheBin) { - self.cachePaths = [ + cargoCachePaths.unshift( path.join(CARGO_HOME, "bin"), path.join(CARGO_HOME, ".crates.toml"), path.join(CARGO_HOME, ".crates2.json"), - ...self.cachePaths, - ]; + ); } const cacheTargets = core.getInput("cache-targets").toLowerCase() || "true"; - if (cacheTargets === "true") { - self.cachePaths.push(...workspaces.map((ws) => ws.target)); + const targetCachePaths = cacheTargets === "true" ? workspaces.map((ws) => ws.target) : []; + const cacheDirectories = core.getInput("cache-directories").trim().split(/\s+/).filter(Boolean); + + const targetKey = core.getInput("target-key"); + const workspaceCrates = core.getInput("cache-workspace-crates").toLowerCase() || "false"; + if (targetKey && cacheTargets !== "true") { + core.warning("`target-key` is ignored because `cache-targets` is not `true`."); + } + if (targetKey && workspaceCrates !== "true") { + core.warning("`target-key` is ignored because `cache-workspace-crates` is not `true`."); } - const cacheDirectories = core.getInput("cache-directories"); - for (const dir of cacheDirectories.trim().split(/\s+/).filter(Boolean)) { + self.targetCacheEnabled = Boolean(targetKey) && cacheTargets === "true" && workspaceCrates === "true"; + if (self.targetCacheEnabled) { + self.cacheKey = baseCacheKey; + self.cachePaths = [...cargoCachePaths, ...cacheDirectories]; + + const targetKeyPrefix = `${baseRestoreKey}-target`; + const targetKeyEnvironment = baseCacheKey.slice(baseRestoreKey.length); + self.targetCachePaths = targetCachePaths; + self.targetCacheKey = `${targetKeyPrefix}${targetKeyEnvironment}-${targetKey}`; + self.targetRestoreKeys = uniqInOrder([`${targetKeyPrefix}${targetKeyEnvironment}-`, `${targetKeyPrefix}-`]); + } else { + self.cacheKey = baseCacheKey; + self.cachePaths = [...cargoCachePaths]; + } + + if (!self.targetCacheEnabled && cacheTargets === "true") { + self.cachePaths.push(...targetCachePaths); + } + + for (const dir of self.targetCacheEnabled ? [] : cacheDirectories) { self.cachePaths.push(dir); } @@ -325,14 +365,26 @@ export class CacheConfig { for (const workspace of this.workspaces) { core.info(` ${workspace.root}`); } - core.info(`Cache Paths:`); + core.info(`${this.targetCacheEnabled ? "Cargo Cache" : "Cache"} Paths:`); for (const path of this.cachePaths) { core.info(` ${path}`); } - core.info(`Restore Key:`); + core.info(`${this.targetCacheEnabled ? "Cargo Restore" : "Restore"} Key:`); core.info(` ${this.restoreKey}`); - core.info(`Cache Key:`); + core.info(`${this.targetCacheEnabled ? "Cargo Cache" : "Cache"} Key:`); core.info(` ${this.cacheKey}`); + if (this.targetCacheEnabled) { + core.info(`Target Cache Paths:`); + for (const path of this.targetCachePaths) { + core.info(` ${path}`); + } + core.info(`Target Restore Keys:`); + for (const key of this.targetRestoreKeys) { + core.info(` ${key}`); + } + core.info(`Target Cache Key:`); + core.info(` ${this.targetCacheKey}`); + } core.info(`.. Prefix:`); core.info(` - ${this.keyPrefix}`); core.info(`.. Environment considered:`); @@ -465,3 +517,7 @@ function sort_and_uniq(a: string[]) { return accumulator; }, []); } + +function uniqInOrder(a: string[]) { + return a.filter((value, index) => a.indexOf(value) === index); +} diff --git a/src/restore.ts b/src/restore.ts index a91572a..d9e7a76 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -2,7 +2,7 @@ import * as core from "@actions/core"; import { cleanTargetDir } from "./cleanup"; import { CacheConfig } from "./config"; -import { getCacheProvider, reportError } from "./utils"; +import { CacheProvider, getCacheProvider, reportError } from "./utils"; process.on("uncaughtException", (e) => { core.error(e.message); @@ -20,11 +20,11 @@ async function run() { } try { - var cacheOnFailure = core.getInput("cache-on-failure").toLowerCase(); + let cacheOnFailure = core.getInput("cache-on-failure").toLowerCase(); if (cacheOnFailure !== "true") { cacheOnFailure = "false"; } - var lookupOnly = core.getInput("lookup-only").toLowerCase() === "true"; + const lookupOnly = core.getInput("lookup-only").toLowerCase() === "true"; core.exportVariable("CACHE_ON_FAILURE", cacheOnFailure); core.exportVariable("CARGO_INCREMENTAL", 0); @@ -34,36 +34,42 @@ async function run() { core.info(""); core.info(`... ${lookupOnly ? "Checking" : "Restoring"} cache ...`); - const key = config.cacheKey; - // Pass a copy of cachePaths to avoid mutating the original array as reported by: - // https://github.com/actions/toolkit/pull/1378 - // TODO: remove this once the underlying bug is fixed. - const restoreKey = await cacheProvider.cache.restoreCache(config.cachePaths.slice(), key, [config.restoreKey], { - lookupOnly, - }); - if (restoreKey) { - const match = - restoreKey.localeCompare(key, undefined, { - sensitivity: "accent", - }) === 0; - core.info(`${lookupOnly ? "Found" : "Restored from"} cache key "${restoreKey}" full match: ${match}.`); - if (!match) { - // pre-clean the target directory on cache mismatch - for (const workspace of config.workspaces) { - try { - await cleanTargetDir(workspace.target, [], true); - } catch {} - } + const cacheResult = await restoreCache(cacheProvider, config.cachePaths, config.cacheKey, [config.restoreKey], lookupOnly); + config.cacheNeedsSave = !cacheResult.match; + if (config.targetCacheEnabled) { + if (cacheResult.found && !cacheResult.match) { + // pre-clean the target directory on cargo cache mismatch before restoring target cache + await cleanTargets(config); + } - // We restored the cache but it is not a full match. + const targetResult = await restoreCache( + cacheProvider, + config.targetCachePaths, + config.targetCacheKey, + config.targetRestoreKeys, + lookupOnly, + "target", + ); + config.targetCacheNeedsSave = !targetResult.match; + if (targetResult.found && !targetResult.match) { + // pre-clean the target directory on target cache mismatch + await cleanTargets(config); + } + + if (!cacheResult.match || !targetResult.match) { config.saveState(); } - setCacheHitOutput(match); + setCacheHitOutput(cacheResult.match && targetResult.match); + } else if (cacheResult.match) { + setCacheHitOutput(true); } else { - core.info("No cache found."); - config.saveState(); + if (cacheResult.found) { + // pre-clean the target directory on cache mismatch + await cleanTargets(config); + } + config.saveState(); setCacheHitOutput(false); } } catch (e) { @@ -78,4 +84,45 @@ function setCacheHitOutput(cacheHit: boolean): void { core.setOutput("cache-hit", cacheHit.toString()); } +async function restoreCache( + cacheProvider: CacheProvider, + paths: string[], + key: string, + restoreKeys: string[], + lookupOnly: boolean, + name = "", +): Promise { + const label = name ? `${name} cache` : "cache"; + // Pass a copy of cachePaths to avoid mutating the original array as reported by: + // https://github.com/actions/toolkit/pull/1378 + // TODO: remove this once the underlying bug is fixed. + const restoreKey = await cacheProvider.cache.restoreCache(paths.slice(), key, restoreKeys, { + lookupOnly, + }); + if (!restoreKey) { + core.info(`No ${label} found.`); + return { found: false, match: false }; + } + + const match = + restoreKey.localeCompare(key, undefined, { + sensitivity: "accent", + }) === 0; + core.info(`${lookupOnly ? "Found" : "Restored from"} ${label} key "${restoreKey}" full match: ${match}.`); + return { found: true, match }; +} + +interface RestoreResult { + found: boolean; + match: boolean; +} + +async function cleanTargets(config: CacheConfig) { + for (const workspace of config.workspaces) { + try { + await cleanTargetDir(workspace.target, [], true); + } catch {} + } +} + run(); diff --git a/src/save.ts b/src/save.ts index 5bd794e..8ed5746 100644 --- a/src/save.ts +++ b/src/save.ts @@ -3,7 +3,7 @@ import * as exec from "@actions/exec"; import { cleanBin, cleanGit, cleanRegistry, cleanTargetDir } from "./cleanup"; import { CacheConfig, isCacheUpToDate } from "./config"; -import { getCacheProvider, reportError } from "./utils"; +import { CacheProvider, getCacheProvider, reportError } from "./utils"; process.on("uncaughtException", (e) => { core.error(e.message); @@ -36,6 +36,8 @@ async function run() { await macOsWorkaround(); } + const cleanCargo = !config.targetCacheEnabled || config.cacheNeedsSave; + const cleanTargets = !config.targetCacheEnabled || config.targetCacheNeedsSave; const workspaceCrates = core.getInput("cache-workspace-crates").toLowerCase() || "false"; const allPackages = []; for (const workspace of config.workspaces) { @@ -45,43 +47,60 @@ async function run() { packages.push(...wsMembers); } allPackages.push(...packages); + if (cleanTargets) { + try { + core.info(`... Cleaning ${workspace.target} ...`); + await cleanTargetDir(workspace.target, packages); + } catch (e) { + core.debug(`${(e as any).stack}`); + } + } + } + + if (cleanCargo) { try { - core.info(`... Cleaning ${workspace.target} ...`); - await cleanTargetDir(workspace.target, packages); + const crates = core.getInput("cache-all-crates").toLowerCase() || "false"; + core.info(`... Cleaning cargo registry (cache-all-crates: ${crates}) ...`); + await cleanRegistry(allPackages, crates !== "true"); + } catch (e) { + core.debug(`${(e as any).stack}`); + } + + if (config.cacheBin) { + try { + core.info(`... Cleaning cargo/bin ...`); + await cleanBin(config.cargoBins); + } catch (e) { + core.debug(`${(e as any).stack}`); + } + } + + try { + core.info(`... Cleaning cargo git cache ...`); + await cleanGit(allPackages); } catch (e) { core.debug(`${(e as any).stack}`); } } - try { - const crates = core.getInput("cache-all-crates").toLowerCase() || "false"; - core.info(`... Cleaning cargo registry (cache-all-crates: ${crates}) ...`); - await cleanRegistry(allPackages, crates !== "true"); - } catch (e) { - core.debug(`${(e as any).stack}`); - } - - if (config.cacheBin) { - try { - core.info(`... Cleaning cargo/bin ...`); - await cleanBin(config.cargoBins); - } catch (e) { - core.debug(`${(e as any).stack}`); + if (config.targetCacheEnabled) { + if (config.cacheNeedsSave) { + core.info(`... Saving cargo cache ...`); + await saveCache(cacheProvider, config.cachePaths, config.cacheKey); + } else { + core.info(`Cargo cache up-to-date.`); } - } - try { - core.info(`... Cleaning cargo git cache ...`); - await cleanGit(allPackages); - } catch (e) { - core.debug(`${(e as any).stack}`); + if (config.targetCacheNeedsSave) { + core.info(`... Saving target cache ...`); + await saveCache(cacheProvider, config.targetCachePaths, config.targetCacheKey); + } else { + core.info(`Target cache up-to-date.`); + } + } else { + core.info(`... Saving cache ...`); + await saveCache(cacheProvider, config.cachePaths, config.cacheKey); } - - core.info(`... Saving cache ...`); - // Pass a copy of cachePaths to avoid mutating the original array as reported by: - // https://github.com/actions/toolkit/pull/1378 - // TODO: remove this once the underlying bug is fixed. - await cacheProvider.cache.saveCache(config.cachePaths.slice(), config.cacheKey); } catch (e) { reportError(e); } @@ -90,6 +109,13 @@ async function run() { run(); +async function saveCache(cacheProvider: CacheProvider, paths: string[], key: string) { + // Pass a copy of cachePaths to avoid mutating the original array as reported by: + // https://github.com/actions/toolkit/pull/1378 + // TODO: remove this once the underlying bug is fixed. + await cacheProvider.cache.saveCache(paths.slice(), key); +} + async function macOsWorkaround() { try { // Workaround for https://github.com/actions/cache/issues/403