3 Commits

Author SHA1 Message Date
Arpad Borsos
988c164c3d 2.4.0
Some checks failed
coverage / Test `cargo-llvm-cov` on ubuntu-latest (push) Failing after 0s
globbed-directories / Test `cargo check/test` on ubuntu-latest (push) Failing after 0s
install / Test `cargo install` on ubuntu-latest (push) Failing after 0s
simple / Test `cargo check/test` on ubuntu-latest (push) Failing after 0s
sparse-registry / Test `cargo check/test` with sparse registry on ubuntu-latest (push) Failing after 0s
target-dir / Test custom target-dir on ubuntu-latest (push) Failing after 0s
workspaces / Test multiple workspaces on ubuntu-latest (push) Failing after 0s
coverage / Test `cargo-llvm-cov` on macos-latest (push) Has been cancelled
coverage / Test `cargo-llvm-cov` on windows-latest (push) Has been cancelled
install / Test `cargo install` on macos-latest (push) Has been cancelled
install / Test `cargo install` on windows-latest (push) Has been cancelled
simple / Test `cargo check/test` on macos-latest (push) Has been cancelled
simple / Test `cargo check/test` on windows-latest (push) Has been cancelled
sparse-registry / Test `cargo check/test` with sparse registry on macos-latest (push) Has been cancelled
sparse-registry / Test `cargo check/test` with sparse registry on windows-latest (push) Has been cancelled
target-dir / Test custom target-dir on macos-latest (push) Has been cancelled
target-dir / Test custom target-dir on windows-latest (push) Has been cancelled
workspaces / Test multiple workspaces on macos-latest (push) Has been cancelled
workspaces / Test multiple workspaces on windows-latest (push) Has been cancelled
2023-05-21 19:36:24 +02:00
Steven Hartland
bb80d0f127 chore: use 8 character hash components (#143)
Use 8 character hash components to reduce the key length, making it
more readable.

Fixes #97
2023-05-19 20:30:37 +02:00
Steven Hartland
ad97570a01 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
2023-05-18 22:48:40 +02:00
11 changed files with 298 additions and 181 deletions

View File

@@ -1,5 +1,10 @@
# Changelog
## 2.4.0
- Fix cache key stability.
- Use 8 character hash components to reduce the key length, making it more readable.
## 2.3.0
- Add `cache-all-crates` option, which enables caching of crates installed by workflows.

View File

@@ -101,7 +101,6 @@ This cache is automatically keyed by:
- the value of some compiler-specific environment variables (eg. RUSTFLAGS, etc), and
- a hash of all `Cargo.lock` / `Cargo.toml` files found anywhere in the repository (if present).
- a hash of all `rust-toolchain` / `rust-toolchain.toml` files in the root of the repository (if present).
- a hash of installed packages as generated by `cargo install --list`.
An additional input `key` can be provided if the builtin keys are not sufficient.
@@ -137,7 +136,7 @@ otherwise corrupt the cache on macOS builds.
This specialized cache action is built on top of the upstream cache action
maintained by GitHub. The same restrictions and limits apply, which are
documented here:
https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows
[Caching dependencies to speed up workflows](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows)
In particular, caches are currently limited to 10 GB in total and exceeding that
limit will cause eviction of older caches.

135
dist/restore/index.js vendored
View File

@@ -59977,8 +59977,8 @@ async function getCmdOutput(cmd, args = [], options = {}) {
});
}
catch (e) {
lib_core.info(`[warning] Command failed: ${cmd} ${args.join(" ")}`);
lib_core.info(`[warning] ${stderr}`);
lib_core.error(`Command failed: ${cmd} ${args.join(" ")}`);
lib_core.error(stderr);
throw e;
}
return stdout;
@@ -60024,12 +60024,11 @@ class Workspace {
const HOME = external_os_default().homedir();
const config_CARGO_HOME = process.env.CARGO_HOME || external_path_default().join(HOME, ".cargo");
const STATE_LOCKFILE_HASH = "RUST_CACHE_LOCKFILE_HASH";
const STATE_LOCKFILES = "RUST_CACHE_LOCKFILES";
const config_STATE_BINS = "RUST_CACHE_BINS";
const STATE_KEY = "RUST_CACHE_KEY";
const STATE_CONFIG = "RUST_CACHE_CONFIG";
const HASH_LENGTH = 8;
class CacheConfig {
constructor() {
/** All the paths we want to cache */
@@ -60040,6 +60039,8 @@ class CacheConfig {
this.restoreKey = "";
/** The workspace configurations */
this.workspaces = [];
/** The cargo binaries present during main step */
this.cargoBins = [];
/** The prefix portion of the cache key */
this.keyPrefix = "";
/** The rust version considered for the cache key */
@@ -60103,20 +60104,11 @@ class CacheConfig {
}
}
self.keyEnvs = keyEnvs;
// Installed packages and their versions are also considered for the key.
const packages = await getPackages();
hasher.update(packages);
key += `-${hasher.digest("hex")}`;
key += `-${digest(hasher)}`;
self.restoreKey = key;
// Construct the lockfiles portion of the key:
// This considers all the files found via globbing for various manifests
// and lockfiles.
// This part is computed in the "pre"/"restore" part of the job and persisted
// into the `state`. That state is loaded in the "post"/"save" part of the
// job so we have consistent values even though the "main" actions run
// might create/overwrite lockfiles.
let lockHash = lib_core.getState(STATE_LOCKFILE_HASH);
let keyFiles = JSON.parse(lib_core.getState(STATE_LOCKFILES) || "[]");
// Constructs the workspace config and paths to restore:
// The workspaces are given using a `$workspace -> $target` syntax.
const workspaces = [];
@@ -60128,24 +60120,20 @@ class CacheConfig {
workspaces.push(new Workspace(root, target));
}
self.workspaces = workspaces;
if (!lockHash) {
keyFiles = keyFiles.concat(await globFiles("rust-toolchain\nrust-toolchain.toml"));
for (const workspace of workspaces) {
const root = workspace.root;
keyFiles.push(...(await globFiles(`${root}/**/Cargo.toml\n${root}/**/Cargo.lock\n${root}/**/rust-toolchain\n${root}/**/rust-toolchain.toml`)));
}
keyFiles = keyFiles.filter(file => !external_fs_default().statSync(file).isDirectory());
keyFiles.sort((a, b) => a.localeCompare(b));
hasher = external_crypto_default().createHash("sha1");
for (const file of keyFiles) {
for await (const chunk of external_fs_default().createReadStream(file)) {
hasher.update(chunk);
}
}
lockHash = hasher.digest("hex");
lib_core.saveState(STATE_LOCKFILE_HASH, lockHash);
lib_core.saveState(STATE_LOCKFILES, JSON.stringify(keyFiles));
let keyFiles = await globFiles("rust-toolchain\nrust-toolchain.toml");
for (const workspace of workspaces) {
const root = workspace.root;
keyFiles.push(...(await globFiles(`${root}/**/Cargo.toml\n${root}/**/Cargo.lock\n${root}/**/rust-toolchain\n${root}/**/rust-toolchain.toml`)));
}
keyFiles = keyFiles.filter(file => !external_fs_default().statSync(file).isDirectory());
keyFiles.sort((a, b) => a.localeCompare(b));
hasher = external_crypto_default().createHash("sha1");
for (const file of keyFiles) {
for await (const chunk of external_fs_default().createReadStream(file)) {
hasher.update(chunk);
}
}
let lockHash = digest(hasher);
self.keyFiles = keyFiles;
key += `-${lockHash}`;
self.cacheKey = key;
@@ -60158,8 +60146,32 @@ class CacheConfig {
for (const dir of cacheDirectories.trim().split(/\s+/).filter(Boolean)) {
self.cachePaths.push(dir);
}
const bins = await getCargoBins();
self.cargoBins = Array.from(bins.values());
return self;
}
/**
* Reads and returns the cache config from the action `state`.
*
* @throws {Error} if the state is not present.
* @returns {CacheConfig} the configuration.
* @see {@link CacheConfig#saveState}
* @see {@link CacheConfig#new}
*/
static fromState() {
const source = lib_core.getState(STATE_CONFIG);
if (!source) {
throw new Error("Cache configuration not found in state");
}
const self = new CacheConfig();
Object.assign(self, JSON.parse(source));
self.workspaces = self.workspaces
.map((w) => new Workspace(w.root, w.target));
return self;
}
/**
* Prints the configuration to the action log.
*/
printInfo() {
lib_core.startGroup("Cache Configuration");
lib_core.info(`Workspaces:`);
@@ -60187,6 +60199,30 @@ class CacheConfig {
}
lib_core.endGroup();
}
/**
* Saves the configuration to the state store.
* This is used to restore the configuration in the post action.
*/
saveState() {
lib_core.saveState(STATE_CONFIG, this);
}
}
/**
* Checks if the cache is up to date.
*
* @returns `true` if the cache is up to date, `false` otherwise.
*/
function isCacheUpToDate() {
return core.getState(STATE_CONFIG) === "";
}
/**
* Returns a hex digest of the given hasher truncated to `HASH_LENGTH`.
*
* @param hasher The hasher to digest.
* @returns The hex digest.
*/
function digest(hasher) {
return hasher.digest("hex").substring(0, HASH_LENGTH);
}
async function getRustVersion() {
const stdout = await getCmdOutput("rustc", ["-vV"]);
@@ -60197,11 +60233,6 @@ async function getRustVersion() {
.filter((s) => s.length === 2);
return Object.fromEntries(splits);
}
async function getPackages() {
let stdout = await getCmdOutput("cargo", ["install", "--list"]);
// Make OS independent.
return stdout.split(/[\n\r]+/).join("\n");
}
async function globFiles(pattern) {
const globber = await glob.create(pattern, {
followSymbolicLinks: false,
@@ -60269,9 +60300,14 @@ async function getCargoBins() {
catch { }
return bins;
}
async function cleanBin() {
/**
* Clean the cargo bin directory, removing the binaries that existed
* when the action started, as they were not created by the build.
*
* @param oldBins The binaries that existed when the action started.
*/
async function cleanBin(oldBins) {
const bins = await getCargoBins();
const oldBins = JSON.parse(core.getState(STATE_BINS));
for (const bin of oldBins) {
bins.delete(bin);
}
@@ -60439,9 +60475,9 @@ async function exists(path) {
process.on("uncaughtException", (e) => {
lib_core.info(`[warning] ${e.message}`);
lib_core.error(e.message);
if (e.stack) {
lib_core.info(e.stack);
lib_core.error(e.stack);
}
});
async function run() {
@@ -60459,8 +60495,6 @@ async function run() {
const config = await CacheConfig["new"]();
config.printInfo();
lib_core.info("");
const bins = await getCargoBins();
lib_core.saveState(config_STATE_BINS, JSON.stringify([...bins]));
lib_core.info(`... Restoring cache ...`);
const key = config.cacheKey;
// Pass a copy of cachePaths to avoid mutating the original array as reported by:
@@ -60468,9 +60502,9 @@ async function run() {
// TODO: remove this once the underlying bug is fixed.
const restoreKey = await cache.restoreCache(config.cachePaths.slice(), key, [config.restoreKey]);
if (restoreKey) {
lib_core.info(`Restored from cache key "${restoreKey}".`);
lib_core.saveState(STATE_KEY, restoreKey);
if (restoreKey !== key) {
const match = restoreKey === key;
lib_core.info(`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 {
@@ -60478,17 +60512,20 @@ async function run() {
}
catch { }
}
// We restored the cache but it is not a full match.
config.saveState();
}
setCacheHitOutput(restoreKey === key);
setCacheHitOutput(match);
}
else {
lib_core.info("No cache found.");
config.saveState();
setCacheHitOutput(false);
}
}
catch (e) {
setCacheHitOutput(false);
lib_core.info(`[warning] ${e.stack}`);
lib_core.error(`${e.stack}`);
}
}
function setCacheHitOutput(cacheHit) {

136
dist/save/index.js vendored
View File

@@ -59977,8 +59977,8 @@ async function getCmdOutput(cmd, args = [], options = {}) {
});
}
catch (e) {
core.info(`[warning] Command failed: ${cmd} ${args.join(" ")}`);
core.info(`[warning] ${stderr}`);
core.error(`Command failed: ${cmd} ${args.join(" ")}`);
core.error(stderr);
throw e;
}
return stdout;
@@ -60024,12 +60024,11 @@ class Workspace {
const HOME = external_os_default().homedir();
const CARGO_HOME = process.env.CARGO_HOME || external_path_default().join(HOME, ".cargo");
const STATE_LOCKFILE_HASH = "RUST_CACHE_LOCKFILE_HASH";
const STATE_LOCKFILES = "RUST_CACHE_LOCKFILES";
const STATE_BINS = "RUST_CACHE_BINS";
const STATE_KEY = "RUST_CACHE_KEY";
const STATE_CONFIG = "RUST_CACHE_CONFIG";
const HASH_LENGTH = 8;
class CacheConfig {
constructor() {
/** All the paths we want to cache */
@@ -60040,6 +60039,8 @@ class CacheConfig {
this.restoreKey = "";
/** The workspace configurations */
this.workspaces = [];
/** The cargo binaries present during main step */
this.cargoBins = [];
/** The prefix portion of the cache key */
this.keyPrefix = "";
/** The rust version considered for the cache key */
@@ -60103,20 +60104,11 @@ class CacheConfig {
}
}
self.keyEnvs = keyEnvs;
// Installed packages and their versions are also considered for the key.
const packages = await getPackages();
hasher.update(packages);
key += `-${hasher.digest("hex")}`;
key += `-${digest(hasher)}`;
self.restoreKey = key;
// Construct the lockfiles portion of the key:
// This considers all the files found via globbing for various manifests
// and lockfiles.
// This part is computed in the "pre"/"restore" part of the job and persisted
// into the `state`. That state is loaded in the "post"/"save" part of the
// job so we have consistent values even though the "main" actions run
// might create/overwrite lockfiles.
let lockHash = core.getState(STATE_LOCKFILE_HASH);
let keyFiles = JSON.parse(core.getState(STATE_LOCKFILES) || "[]");
// Constructs the workspace config and paths to restore:
// The workspaces are given using a `$workspace -> $target` syntax.
const workspaces = [];
@@ -60128,24 +60120,20 @@ class CacheConfig {
workspaces.push(new Workspace(root, target));
}
self.workspaces = workspaces;
if (!lockHash) {
keyFiles = keyFiles.concat(await globFiles("rust-toolchain\nrust-toolchain.toml"));
for (const workspace of workspaces) {
const root = workspace.root;
keyFiles.push(...(await globFiles(`${root}/**/Cargo.toml\n${root}/**/Cargo.lock\n${root}/**/rust-toolchain\n${root}/**/rust-toolchain.toml`)));
}
keyFiles = keyFiles.filter(file => !external_fs_default().statSync(file).isDirectory());
keyFiles.sort((a, b) => a.localeCompare(b));
hasher = external_crypto_default().createHash("sha1");
for (const file of keyFiles) {
for await (const chunk of external_fs_default().createReadStream(file)) {
hasher.update(chunk);
}
}
lockHash = hasher.digest("hex");
core.saveState(STATE_LOCKFILE_HASH, lockHash);
core.saveState(STATE_LOCKFILES, JSON.stringify(keyFiles));
let keyFiles = await globFiles("rust-toolchain\nrust-toolchain.toml");
for (const workspace of workspaces) {
const root = workspace.root;
keyFiles.push(...(await globFiles(`${root}/**/Cargo.toml\n${root}/**/Cargo.lock\n${root}/**/rust-toolchain\n${root}/**/rust-toolchain.toml`)));
}
keyFiles = keyFiles.filter(file => !external_fs_default().statSync(file).isDirectory());
keyFiles.sort((a, b) => a.localeCompare(b));
hasher = external_crypto_default().createHash("sha1");
for (const file of keyFiles) {
for await (const chunk of external_fs_default().createReadStream(file)) {
hasher.update(chunk);
}
}
let lockHash = digest(hasher);
self.keyFiles = keyFiles;
key += `-${lockHash}`;
self.cacheKey = key;
@@ -60158,8 +60146,32 @@ class CacheConfig {
for (const dir of cacheDirectories.trim().split(/\s+/).filter(Boolean)) {
self.cachePaths.push(dir);
}
const bins = await getCargoBins();
self.cargoBins = Array.from(bins.values());
return self;
}
/**
* Reads and returns the cache config from the action `state`.
*
* @throws {Error} if the state is not present.
* @returns {CacheConfig} the configuration.
* @see {@link CacheConfig#saveState}
* @see {@link CacheConfig#new}
*/
static fromState() {
const source = core.getState(STATE_CONFIG);
if (!source) {
throw new Error("Cache configuration not found in state");
}
const self = new CacheConfig();
Object.assign(self, JSON.parse(source));
self.workspaces = self.workspaces
.map((w) => new Workspace(w.root, w.target));
return self;
}
/**
* Prints the configuration to the action log.
*/
printInfo() {
core.startGroup("Cache Configuration");
core.info(`Workspaces:`);
@@ -60187,6 +60199,30 @@ class CacheConfig {
}
core.endGroup();
}
/**
* Saves the configuration to the state store.
* This is used to restore the configuration in the post action.
*/
saveState() {
core.saveState(STATE_CONFIG, this);
}
}
/**
* Checks if the cache is up to date.
*
* @returns `true` if the cache is up to date, `false` otherwise.
*/
function isCacheUpToDate() {
return core.getState(STATE_CONFIG) === "";
}
/**
* Returns a hex digest of the given hasher truncated to `HASH_LENGTH`.
*
* @param hasher The hasher to digest.
* @returns The hex digest.
*/
function digest(hasher) {
return hasher.digest("hex").substring(0, HASH_LENGTH);
}
async function getRustVersion() {
const stdout = await getCmdOutput("rustc", ["-vV"]);
@@ -60197,11 +60233,6 @@ async function getRustVersion() {
.filter((s) => s.length === 2);
return Object.fromEntries(splits);
}
async function getPackages() {
let stdout = await getCmdOutput("cargo", ["install", "--list"]);
// Make OS independent.
return stdout.split(/[\n\r]+/).join("\n");
}
async function globFiles(pattern) {
const globber = await glob.create(pattern, {
followSymbolicLinks: false,
@@ -60269,9 +60300,14 @@ async function getCargoBins() {
catch { }
return bins;
}
async function cleanBin() {
/**
* Clean the cargo bin directory, removing the binaries that existed
* when the action started, as they were not created by the build.
*
* @param oldBins The binaries that existed when the action started.
*/
async function cleanBin(oldBins) {
const bins = await getCargoBins();
const oldBins = JSON.parse(core.getState(STATE_BINS));
for (const bin of oldBins) {
bins.delete(bin);
}
@@ -60440,9 +60476,9 @@ async function exists(path) {
process.on("uncaughtException", (e) => {
core.info(`[warning] ${e.message}`);
core.error(e.message);
if (e.stack) {
core.info(e.stack);
core.error(e.stack);
}
});
async function run() {
@@ -60451,13 +60487,13 @@ async function run() {
return;
}
try {
const config = await CacheConfig["new"]();
config.printInfo();
core.info("");
if (core.getState(STATE_KEY) === config.cacheKey) {
if (isCacheUpToDate()) {
core.info(`Cache up-to-date.`);
return;
}
const config = CacheConfig.fromState();
config.printInfo();
core.info("");
// TODO: remove this once https://github.com/actions/toolkit/pull/553 lands
await macOsWorkaround();
const allPackages = [];
@@ -60473,16 +60509,16 @@ async function run() {
}
}
try {
const creates = core.getInput("cache-all-crates").toLowerCase() || "false";
core.info(`... Cleaning cargo registry cache-all-crates: ${creates} ...`);
await cleanRegistry(allPackages, creates === "true");
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.error(`${e.stack}`);
}
try {
core.info(`... Cleaning cargo/bin ...`);
await cleanBin();
await cleanBin(config.cargoBins);
}
catch (e) {
core.error(`${e.stack}`);

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "rust-cache",
"version": "2.3.0",
"version": "2.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "rust-cache",
"version": "2.3.0",
"version": "2.4.0",
"license": "LGPL-3.0",
"dependencies": {
"@actions/cache": "^3.2.1",

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "rust-cache",
"version": "2.3.0",
"version": "2.4.0",
"description": "A GitHub Action that implements smart caching for rust/cargo projects with sensible defaults.",
"keywords": [
"actions",

View File

@@ -3,7 +3,7 @@ import * as io from "@actions/io";
import fs from "fs";
import path from "path";
import { CARGO_HOME, STATE_BINS } from "./config";
import { CARGO_HOME } from "./config";
import { Packages } from "./workspace";
export async function cleanTargetDir(targetDir: string, packages: Packages, checkTimestamp = false) {
@@ -69,9 +69,14 @@ export async function getCargoBins(): Promise<Set<string>> {
return bins;
}
export async function cleanBin() {
/**
* Clean the cargo bin directory, removing the binaries that existed
* when the action started, as they were not created by the build.
*
* @param oldBins The binaries that existed when the action started.
*/
export async function cleanBin(oldBins: Array<string>) {
const bins = await getCargoBins();
const oldBins = JSON.parse(core.getState(STATE_BINS));
for (const bin of oldBins) {
bins.delete(bin);
@@ -186,10 +191,10 @@ const ONE_WEEK = 7 * 24 * 3600 * 1000;
/**
* Removes all files or directories in `dirName` matching some criteria.
*
*
* When the `checkTimestamp` flag is set, this will also remove anything older
* than one week.
*
*
* Otherwise, it will remove everything that does not match any string in the
* `keepPrefix` set.
* The matching strips and trailing `-$hash` suffix.

View File

@@ -7,14 +7,13 @@ import path from "path";
import { getCmdOutput } from "./utils";
import { Workspace } from "./workspace";
import { getCargoBins } from "./cleanup";
const HOME = os.homedir();
export const CARGO_HOME = process.env.CARGO_HOME || path.join(HOME, ".cargo");
const STATE_LOCKFILE_HASH = "RUST_CACHE_LOCKFILE_HASH";
const STATE_LOCKFILES = "RUST_CACHE_LOCKFILES";
export const STATE_BINS = "RUST_CACHE_BINS";
export const STATE_KEY = "RUST_CACHE_KEY";
const STATE_CONFIG = "RUST_CACHE_CONFIG";
const HASH_LENGTH = 8;
export class CacheConfig {
/** All the paths we want to cache */
@@ -27,6 +26,9 @@ export class CacheConfig {
/** The workspace configurations */
public workspaces: Array<Workspace> = [];
/** The cargo binaries present during main step */
public cargoBins: Array<string> = [];
/** The prefix portion of the cache key */
private keyPrefix = "";
/** The rust version considered for the cache key */
@@ -104,24 +106,13 @@ export class CacheConfig {
self.keyEnvs = keyEnvs;
// Installed packages and their versions are also considered for the key.
const packages = await getPackages();
hasher.update(packages);
key += `-${hasher.digest("hex")}`;
key += `-${digest(hasher)}`;
self.restoreKey = key;
// Construct the lockfiles portion of the key:
// This considers all the files found via globbing for various manifests
// and lockfiles.
// This part is computed in the "pre"/"restore" part of the job and persisted
// into the `state`. That state is loaded in the "post"/"save" part of the
// job so we have consistent values even though the "main" actions run
// might create/overwrite lockfiles.
let lockHash = core.getState(STATE_LOCKFILE_HASH);
let keyFiles: Array<string> = JSON.parse(core.getState(STATE_LOCKFILES) || "[]");
// Constructs the workspace config and paths to restore:
// The workspaces are given using a `$workspace -> $target` syntax.
@@ -136,30 +127,25 @@ export class CacheConfig {
}
self.workspaces = workspaces;
if (!lockHash) {
keyFiles = keyFiles.concat(await globFiles("rust-toolchain\nrust-toolchain.toml"));
for (const workspace of workspaces) {
const root = workspace.root;
keyFiles.push(
...(await globFiles(
`${root}/**/Cargo.toml\n${root}/**/Cargo.lock\n${root}/**/rust-toolchain\n${root}/**/rust-toolchain.toml`,
)),
);
}
keyFiles = keyFiles.filter(file => !fs.statSync(file).isDirectory());
keyFiles.sort((a, b) => a.localeCompare(b));
hasher = crypto.createHash("sha1");
for (const file of keyFiles) {
for await (const chunk of fs.createReadStream(file)) {
hasher.update(chunk);
}
}
lockHash = hasher.digest("hex");
core.saveState(STATE_LOCKFILE_HASH, lockHash);
core.saveState(STATE_LOCKFILES, JSON.stringify(keyFiles));
let keyFiles = await globFiles("rust-toolchain\nrust-toolchain.toml");
for (const workspace of workspaces) {
const root = workspace.root;
keyFiles.push(
...(await globFiles(
`${root}/**/Cargo.toml\n${root}/**/Cargo.lock\n${root}/**/rust-toolchain\n${root}/**/rust-toolchain.toml`,
)),
);
}
keyFiles = keyFiles.filter(file => !fs.statSync(file).isDirectory());
keyFiles.sort((a, b) => a.localeCompare(b));
hasher = crypto.createHash("sha1");
for (const file of keyFiles) {
for await (const chunk of fs.createReadStream(file)) {
hasher.update(chunk);
}
}
let lockHash = digest(hasher);
self.keyFiles = keyFiles;
@@ -177,9 +163,37 @@ export class CacheConfig {
self.cachePaths.push(dir);
}
const bins = await getCargoBins();
self.cargoBins = Array.from(bins.values());
return self;
}
/**
* Reads and returns the cache config from the action `state`.
*
* @throws {Error} if the state is not present.
* @returns {CacheConfig} the configuration.
* @see {@link CacheConfig#saveState}
* @see {@link CacheConfig#new}
*/
static fromState(): CacheConfig {
const source = core.getState(STATE_CONFIG);
if (!source) {
throw new Error("Cache configuration not found in state");
}
const self = new CacheConfig();
Object.assign(self, JSON.parse(source));
self.workspaces = self.workspaces
.map((w: any) => new Workspace(w.root, w.target));
return self;
}
/**
* Prints the configuration to the action log.
*/
printInfo() {
core.startGroup("Cache Configuration");
core.info(`Workspaces:`);
@@ -207,6 +221,33 @@ export class CacheConfig {
}
core.endGroup();
}
/**
* Saves the configuration to the state store.
* This is used to restore the configuration in the post action.
*/
saveState() {
core.saveState(STATE_CONFIG, this);
}
}
/**
* Checks if the cache is up to date.
*
* @returns `true` if the cache is up to date, `false` otherwise.
*/
export function isCacheUpToDate(): boolean {
return core.getState(STATE_CONFIG) === "";
}
/**
* Returns a hex digest of the given hasher truncated to `HASH_LENGTH`.
*
* @param hasher The hasher to digest.
* @returns The hex digest.
*/
function digest(hasher: crypto.Hash): string {
return hasher.digest("hex").substring(0, HASH_LENGTH);
}
interface RustVersion {
@@ -225,12 +266,6 @@ async function getRustVersion(): Promise<RustVersion> {
return Object.fromEntries(splits);
}
async function getPackages(): Promise<string> {
let stdout = await getCmdOutput("cargo", ["install", "--list"]);
// Make OS independent.
return stdout.split(/[\n\r]+/).join("\n");
}
async function globFiles(pattern: string): Promise<string[]> {
const globber = await glob.create(pattern, {
followSymbolicLinks: false,

View File

@@ -1,13 +1,13 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { cleanTargetDir, getCargoBins } from "./cleanup";
import { CacheConfig, STATE_BINS, STATE_KEY } from "./config";
import { cleanTargetDir } from "./cleanup";
import { CacheConfig } from "./config";
process.on("uncaughtException", (e) => {
core.info(`[warning] ${e.message}`);
core.error(e.message);
if (e.stack) {
core.info(e.stack);
core.error(e.stack);
}
});
@@ -29,9 +29,6 @@ async function run() {
config.printInfo();
core.info("");
const bins = await getCargoBins();
core.saveState(STATE_BINS, JSON.stringify([...bins]));
core.info(`... Restoring cache ...`);
const key = config.cacheKey;
// Pass a copy of cachePaths to avoid mutating the original array as reported by:
@@ -39,28 +36,31 @@ async function run() {
// TODO: remove this once the underlying bug is fixed.
const restoreKey = await cache.restoreCache(config.cachePaths.slice(), key, [config.restoreKey]);
if (restoreKey) {
core.info(`Restored from cache key "${restoreKey}".`);
core.saveState(STATE_KEY, restoreKey);
if (restoreKey !== key) {
const match = restoreKey === key;
core.info(`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.
config.saveState();
}
setCacheHitOutput(restoreKey === key);
setCacheHitOutput(match);
} else {
core.info("No cache found.");
config.saveState();
setCacheHitOutput(false);
}
} catch (e) {
setCacheHitOutput(false);
core.info(`[warning] ${(e as any).stack}`);
core.error(`${(e as any).stack}`);
}
}

View File

@@ -3,12 +3,12 @@ import * as core from "@actions/core";
import * as exec from "@actions/exec";
import { cleanBin, cleanGit, cleanRegistry, cleanTargetDir } from "./cleanup";
import { CacheConfig, STATE_KEY } from "./config";
import { CacheConfig, isCacheUpToDate } from "./config";
process.on("uncaughtException", (e) => {
core.info(`[warning] ${e.message}`);
core.error(e.message);
if (e.stack) {
core.info(e.stack);
core.error(e.stack);
}
});
@@ -20,15 +20,15 @@ async function run() {
}
try {
const config = await CacheConfig.new();
config.printInfo();
core.info("");
if (core.getState(STATE_KEY) === config.cacheKey) {
if (isCacheUpToDate()) {
core.info(`Cache up-to-date.`);
return;
}
const config = CacheConfig.fromState();
config.printInfo();
core.info("");
// TODO: remove this once https://github.com/actions/toolkit/pull/553 lands
await macOsWorkaround();
@@ -45,16 +45,16 @@ async function run() {
}
try {
const creates = core.getInput("cache-all-crates").toLowerCase() || "false";
core.info(`... Cleaning cargo registry cache-all-crates: ${creates} ...`);
await cleanRegistry(allPackages, creates === "true");
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.error(`${(e as any).stack}`);
}
try {
core.info(`... Cleaning cargo/bin ...`);
await cleanBin();
await cleanBin(config.cargoBins);
} catch (e) {
core.error(`${(e as any).stack}`);
}

View File

@@ -22,8 +22,8 @@ export async function getCmdOutput(
...options,
});
} catch (e) {
core.info(`[warning] Command failed: ${cmd} ${args.join(" ")}`);
core.info(`[warning] ${stderr}`);
core.error(`Command failed: ${cmd} ${args.join(" ")}`);
core.error(stderr);
throw e;
}
return stdout;