Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/target-key.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dist/cache-Cb-Up9r2.js → dist/cache-BSoAyDaq.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion dist/cache-1jS6aShy.js → dist/cache-D5WyUDMY.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
80 changes: 63 additions & 17 deletions dist/cleanup-ChNUL7jL.js → dist/cleanup-ctNqmXyy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.`);
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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`.");
}
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);
}
const cacheDirectories = getInput("cache-directories");
for (const dir of cacheDirectories.trim().split(/\s+/).filter(Boolean)) {
for (const dir of self.targetCacheEnabled ? [] : cacheDirectories) {
self.cachePaths.push(dir);
}
const bins = await getCargoBins();
Expand Down Expand Up @@ -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:`);
Expand Down Expand Up @@ -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}"`);
Expand Down
77 changes: 51 additions & 26 deletions dist/restore.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
}
Expand All @@ -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();
Loading