Skip to content

Commit ad97570

Browse files
authored
fix: cache key stability (#142)
Ensure consistency of main and post configuration by storing and restoring it from state, which in turn ensures cache key stability. Also: * Fixed some typos. * Use core.error for logging errors. * Fix inverted condition on cache-all-crates. Reverts: #138 Fixes #140
1 parent 060bda3 commit ad97570

File tree

9 files changed

+257
-172
lines changed

9 files changed

+257
-172
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 2.3.1
4+
5+
- Fix cache key stability.
6+
37
## 2.3.0
48

59
- Add `cache-all-crates` option, which enables caching of crates installed by workflows.

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ This cache is automatically keyed by:
101101
- the value of some compiler-specific environment variables (eg. RUSTFLAGS, etc), and
102102
- a hash of all `Cargo.lock` / `Cargo.toml` files found anywhere in the repository (if present).
103103
- a hash of all `rust-toolchain` / `rust-toolchain.toml` files in the root of the repository (if present).
104-
- a hash of installed packages as generated by `cargo install --list`.
105104

106105
An additional input `key` can be provided if the builtin keys are not sufficient.
107106

@@ -137,7 +136,7 @@ otherwise corrupt the cache on macOS builds.
137136
This specialized cache action is built on top of the upstream cache action
138137
maintained by GitHub. The same restrictions and limits apply, which are
139138
documented here:
140-
https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows
139+
[Caching dependencies to speed up workflows](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows)
141140

142141
In particular, caches are currently limited to 10 GB in total and exceeding that
143142
limit will cause eviction of older caches.

dist/restore/index.js

Lines changed: 74 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -59977,8 +59977,8 @@ async function getCmdOutput(cmd, args = [], options = {}) {
5997759977
});
5997859978
}
5997959979
catch (e) {
59980-
lib_core.info(`[warning] Command failed: ${cmd} ${args.join(" ")}`);
59981-
lib_core.info(`[warning] ${stderr}`);
59980+
lib_core.error(`Command failed: ${cmd} ${args.join(" ")}`);
59981+
lib_core.error(stderr);
5998259982
throw e;
5998359983
}
5998459984
return stdout;
@@ -60024,12 +60024,10 @@ class Workspace {
6002460024

6002560025

6002660026

60027+
6002760028
const HOME = external_os_default().homedir();
6002860029
const config_CARGO_HOME = process.env.CARGO_HOME || external_path_default().join(HOME, ".cargo");
60029-
const STATE_LOCKFILE_HASH = "RUST_CACHE_LOCKFILE_HASH";
60030-
const STATE_LOCKFILES = "RUST_CACHE_LOCKFILES";
60031-
const config_STATE_BINS = "RUST_CACHE_BINS";
60032-
const STATE_KEY = "RUST_CACHE_KEY";
60030+
const STATE_CONFIG = "RUST_CACHE_CONFIG";
6003360031
class CacheConfig {
6003460032
constructor() {
6003560033
/** All the paths we want to cache */
@@ -60040,6 +60038,8 @@ class CacheConfig {
6004060038
this.restoreKey = "";
6004160039
/** The workspace configurations */
6004260040
this.workspaces = [];
60041+
/** The cargo binaries present during main step */
60042+
this.cargoBins = [];
6004360043
/** The prefix portion of the cache key */
6004460044
this.keyPrefix = "";
6004560045
/** The rust version considered for the cache key */
@@ -60103,20 +60103,11 @@ class CacheConfig {
6010360103
}
6010460104
}
6010560105
self.keyEnvs = keyEnvs;
60106-
// Installed packages and their versions are also considered for the key.
60107-
const packages = await getPackages();
60108-
hasher.update(packages);
6010960106
key += `-${hasher.digest("hex")}`;
6011060107
self.restoreKey = key;
6011160108
// Construct the lockfiles portion of the key:
6011260109
// This considers all the files found via globbing for various manifests
6011360110
// and lockfiles.
60114-
// This part is computed in the "pre"/"restore" part of the job and persisted
60115-
// into the `state`. That state is loaded in the "post"/"save" part of the
60116-
// job so we have consistent values even though the "main" actions run
60117-
// might create/overwrite lockfiles.
60118-
let lockHash = lib_core.getState(STATE_LOCKFILE_HASH);
60119-
let keyFiles = JSON.parse(lib_core.getState(STATE_LOCKFILES) || "[]");
6012060111
// Constructs the workspace config and paths to restore:
6012160112
// The workspaces are given using a `$workspace -> $target` syntax.
6012260113
const workspaces = [];
@@ -60128,24 +60119,20 @@ class CacheConfig {
6012860119
workspaces.push(new Workspace(root, target));
6012960120
}
6013060121
self.workspaces = workspaces;
60131-
if (!lockHash) {
60132-
keyFiles = keyFiles.concat(await globFiles("rust-toolchain\nrust-toolchain.toml"));
60133-
for (const workspace of workspaces) {
60134-
const root = workspace.root;
60135-
keyFiles.push(...(await globFiles(`${root}/**/Cargo.toml\n${root}/**/Cargo.lock\n${root}/**/rust-toolchain\n${root}/**/rust-toolchain.toml`)));
60136-
}
60137-
keyFiles = keyFiles.filter(file => !external_fs_default().statSync(file).isDirectory());
60138-
keyFiles.sort((a, b) => a.localeCompare(b));
60139-
hasher = external_crypto_default().createHash("sha1");
60140-
for (const file of keyFiles) {
60141-
for await (const chunk of external_fs_default().createReadStream(file)) {
60142-
hasher.update(chunk);
60143-
}
60122+
let keyFiles = await globFiles("rust-toolchain\nrust-toolchain.toml");
60123+
for (const workspace of workspaces) {
60124+
const root = workspace.root;
60125+
keyFiles.push(...(await globFiles(`${root}/**/Cargo.toml\n${root}/**/Cargo.lock\n${root}/**/rust-toolchain\n${root}/**/rust-toolchain.toml`)));
60126+
}
60127+
keyFiles = keyFiles.filter(file => !external_fs_default().statSync(file).isDirectory());
60128+
keyFiles.sort((a, b) => a.localeCompare(b));
60129+
hasher = external_crypto_default().createHash("sha1");
60130+
for (const file of keyFiles) {
60131+
for await (const chunk of external_fs_default().createReadStream(file)) {
60132+
hasher.update(chunk);
6014460133
}
60145-
lockHash = hasher.digest("hex");
60146-
lib_core.saveState(STATE_LOCKFILE_HASH, lockHash);
60147-
lib_core.saveState(STATE_LOCKFILES, JSON.stringify(keyFiles));
6014860134
}
60135+
let lockHash = hasher.digest("hex");
6014960136
self.keyFiles = keyFiles;
6015060137
key += `-${lockHash}`;
6015160138
self.cacheKey = key;
@@ -60158,8 +60145,32 @@ class CacheConfig {
6015860145
for (const dir of cacheDirectories.trim().split(/\s+/).filter(Boolean)) {
6015960146
self.cachePaths.push(dir);
6016060147
}
60148+
const bins = await getCargoBins();
60149+
self.cargoBins = Array.from(bins.values());
6016160150
return self;
6016260151
}
60152+
/**
60153+
* Reads and returns the cache config from the action `state`.
60154+
*
60155+
* @throws {Error} if the state is not present.
60156+
* @returns {CacheConfig} the configuration.
60157+
* @see {@link CacheConfig#saveState}
60158+
* @see {@link CacheConfig#new}
60159+
*/
60160+
static fromState() {
60161+
const source = lib_core.getState(STATE_CONFIG);
60162+
if (!source) {
60163+
throw new Error("Cache configuration not found in state");
60164+
}
60165+
const self = new CacheConfig();
60166+
Object.assign(self, JSON.parse(source));
60167+
self.workspaces = self.workspaces
60168+
.map((w) => new Workspace(w.root, w.target));
60169+
return self;
60170+
}
60171+
/**
60172+
* Prints the configuration to the action log.
60173+
*/
6016360174
printInfo() {
6016460175
lib_core.startGroup("Cache Configuration");
6016560176
lib_core.info(`Workspaces:`);
@@ -60187,6 +60198,21 @@ class CacheConfig {
6018760198
}
6018860199
lib_core.endGroup();
6018960200
}
60201+
/**
60202+
* Saves the configuration to the state store.
60203+
* This is used to restore the configuration in the post action.
60204+
*/
60205+
saveState() {
60206+
lib_core.saveState(STATE_CONFIG, this);
60207+
}
60208+
}
60209+
/**
60210+
* Checks if the cache is up to date.
60211+
*
60212+
* @returns `true` if the cache is up to date, `false` otherwise.
60213+
*/
60214+
function isCacheUpToDate() {
60215+
return core.getState(STATE_CONFIG) === "";
6019060216
}
6019160217
async function getRustVersion() {
6019260218
const stdout = await getCmdOutput("rustc", ["-vV"]);
@@ -60197,11 +60223,6 @@ async function getRustVersion() {
6019760223
.filter((s) => s.length === 2);
6019860224
return Object.fromEntries(splits);
6019960225
}
60200-
async function getPackages() {
60201-
let stdout = await getCmdOutput("cargo", ["install", "--list"]);
60202-
// Make OS independent.
60203-
return stdout.split(/[\n\r]+/).join("\n");
60204-
}
6020560226
async function globFiles(pattern) {
6020660227
const globber = await glob.create(pattern, {
6020760228
followSymbolicLinks: false,
@@ -60269,9 +60290,14 @@ async function getCargoBins() {
6026960290
catch { }
6027060291
return bins;
6027160292
}
60272-
async function cleanBin() {
60293+
/**
60294+
* Clean the cargo bin directory, removing the binaries that existed
60295+
* when the action started, as they were not created by the build.
60296+
*
60297+
* @param oldBins The binaries that existed when the action started.
60298+
*/
60299+
async function cleanBin(oldBins) {
6027360300
const bins = await getCargoBins();
60274-
const oldBins = JSON.parse(core.getState(STATE_BINS));
6027560301
for (const bin of oldBins) {
6027660302
bins.delete(bin);
6027760303
}
@@ -60439,9 +60465,9 @@ async function exists(path) {
6043960465

6044060466

6044160467
process.on("uncaughtException", (e) => {
60442-
lib_core.info(`[warning] ${e.message}`);
60468+
lib_core.error(e.message);
6044360469
if (e.stack) {
60444-
lib_core.info(e.stack);
60470+
lib_core.error(e.stack);
6044560471
}
6044660472
});
6044760473
async function run() {
@@ -60459,36 +60485,37 @@ async function run() {
6045960485
const config = await CacheConfig["new"]();
6046060486
config.printInfo();
6046160487
lib_core.info("");
60462-
const bins = await getCargoBins();
60463-
lib_core.saveState(config_STATE_BINS, JSON.stringify([...bins]));
6046460488
lib_core.info(`... Restoring cache ...`);
6046560489
const key = config.cacheKey;
6046660490
// Pass a copy of cachePaths to avoid mutating the original array as reported by:
6046760491
// https://github.com/actions/toolkit/pull/1378
6046860492
// TODO: remove this once the underlying bug is fixed.
6046960493
const restoreKey = await cache.restoreCache(config.cachePaths.slice(), key, [config.restoreKey]);
6047060494
if (restoreKey) {
60471-
lib_core.info(`Restored from cache key "${restoreKey}".`);
60472-
lib_core.saveState(STATE_KEY, restoreKey);
60473-
if (restoreKey !== key) {
60495+
const match = restoreKey === key;
60496+
lib_core.info(`Restored from cache key "${restoreKey}" full match: ${match}.`);
60497+
if (!match) {
6047460498
// pre-clean the target directory on cache mismatch
6047560499
for (const workspace of config.workspaces) {
6047660500
try {
6047760501
await cleanTargetDir(workspace.target, [], true);
6047860502
}
6047960503
catch { }
6048060504
}
60505+
// We restored the cache but it is not a full match.
60506+
config.saveState();
6048160507
}
60482-
setCacheHitOutput(restoreKey === key);
60508+
setCacheHitOutput(match);
6048360509
}
6048460510
else {
6048560511
lib_core.info("No cache found.");
60512+
config.saveState();
6048660513
setCacheHitOutput(false);
6048760514
}
6048860515
}
6048960516
catch (e) {
6049060517
setCacheHitOutput(false);
60491-
lib_core.info(`[warning] ${e.stack}`);
60518+
lib_core.error(`${e.stack}`);
6049260519
}
6049360520
}
6049460521
function setCacheHitOutput(cacheHit) {

0 commit comments

Comments
 (0)