mirror of
https://github.com/astral-sh/setup-uv.git
synced 2026-11-05 01:35:32 +00:00
## Summary The post step logs `uv cache saved with key: ...` (and `Python cache saved with key: ...`) even when the save failed. The log in #769 shows it right after `Warning: Failed to save: ...`: ``` Warning: Failed to save: "C:\Program failed with error: The process 'C:\Program Files\Git\usr\bin\tar.exe' failed with exit code 2 uv cache saved with key: setup-uv-2-x86_64-pc-windows-msvc-windows-11-3.12.12-pruned-... ``` `@actions/cache`'s `saveCache` doesn't throw for these failures. It logs them and returns `-1`, and `saveCacheToKey` ignored the return value. This now checks for `-1` before logging, which is the same check the `actions/cache` save step uses (`if (cacheId != -1) core.info("Cache saved with key: ...")`). `@actions/cache` already logs the reason, so nothing else is logged on failure, and the exit code stays the same. This only changes the log line. It doesn't address the underlying tar/gzip failure in #769. One limitation: with the legacy (v1) cache service, an upload that fails after the cache was reserved still returns the reserved id, so that one case is still reported as saved. That's on the `@actions/cache` side and affects `actions/cache` the same way. Related to #769 ## Test plan - Added `__tests__/cache/save-cache.test.ts`. The `-1` case fails before the change and passes after it. - `npm run all` on Ubuntu (WSL) with Node 24.19.0 / npm 11.17.0: build, Biome (no fixes), packaging, and all 161 tests across 16 suites pass. The regenerated `dist/save-cache/index.cjs` only differs in the changed lines. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
169 lines
5.1 KiB
TypeScript
169 lines
5.1 KiB
TypeScript
import * as fs from "node:fs";
|
|
import * as cache from "@actions/cache";
|
|
import * as core from "@actions/core";
|
|
import * as exec from "@actions/exec";
|
|
import * as pep440 from "@renovatebot/pep440";
|
|
import {
|
|
STATE_CACHE_KEY,
|
|
STATE_CACHE_MATCHED_KEY,
|
|
STATE_PYTHON_CACHE_MATCHED_KEY,
|
|
} from "./cache/restore-cache";
|
|
import { STATE_UV_PATH, STATE_UV_VERSION } from "./utils/constants";
|
|
import { loadInputs, type SetupInputs } from "./utils/inputs";
|
|
import * as log from "./utils/logging";
|
|
|
|
function formatUnexpectedFailure(error: unknown): string {
|
|
if (error instanceof Error) {
|
|
return error.stack ?? error.message;
|
|
}
|
|
return String(error);
|
|
}
|
|
|
|
function failUnexpectedly(event: string, error: unknown): never {
|
|
core.setFailed(`${event}: ${formatUnexpectedFailure(error)}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
process.on("uncaughtException", (error) => {
|
|
failUnexpectedly("Uncaught exception", error);
|
|
});
|
|
|
|
process.on("unhandledRejection", (reason) => {
|
|
failUnexpectedly("Unhandled promise rejection", reason);
|
|
});
|
|
|
|
export async function run(): Promise<void> {
|
|
try {
|
|
const inputs = loadInputs();
|
|
if (inputs.enableCache) {
|
|
if (inputs.saveCache) {
|
|
await saveCache(inputs);
|
|
} else {
|
|
log.info("save-cache is false. Skipping save cache step.");
|
|
}
|
|
// https://github.com/nodejs/node/issues/56645#issuecomment-3924958861
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
// node will stay alive if any promises are not resolved,
|
|
// which is a possibility if HTTP requests are dangling
|
|
// due to retries or timeouts. We know that if we got here
|
|
// that all promises that we care about have successfully
|
|
// resolved, so simply exit with success.
|
|
process.exit(0);
|
|
}
|
|
} catch (error) {
|
|
const err = error as Error;
|
|
core.setFailed(err.message);
|
|
}
|
|
}
|
|
|
|
async function saveCache(inputs: SetupInputs): Promise<void> {
|
|
const cacheKey = core.getState(STATE_CACHE_KEY);
|
|
const matchedKey = core.getState(STATE_CACHE_MATCHED_KEY);
|
|
|
|
if (!cacheKey) {
|
|
log.warning("Error retrieving cache key from state.");
|
|
return;
|
|
}
|
|
if (matchedKey === cacheKey) {
|
|
log.info(`Cache hit occurred on key ${cacheKey}, not saving cache.`);
|
|
} else {
|
|
if (inputs.pruneCache) {
|
|
await pruneCache();
|
|
}
|
|
|
|
const actualCachePath = getUvCachePath(inputs);
|
|
if (!fs.existsSync(actualCachePath)) {
|
|
if (inputs.ignoreNothingToCache) {
|
|
log.info(
|
|
"No cacheable uv cache paths were found. Ignoring because ignore-nothing-to-cache is enabled.",
|
|
);
|
|
} else {
|
|
throw new Error(
|
|
`Cache path ${actualCachePath} does not exist on disk. This likely indicates that there are no dependencies to cache. Consider disabling the cache input if it is not needed.`,
|
|
);
|
|
}
|
|
} else {
|
|
await saveCacheToKey(
|
|
cacheKey,
|
|
actualCachePath,
|
|
STATE_CACHE_MATCHED_KEY,
|
|
"uv cache",
|
|
);
|
|
}
|
|
}
|
|
|
|
if (inputs.cachePython) {
|
|
if (!fs.existsSync(inputs.pythonDir)) {
|
|
log.warning(
|
|
`Python cache path ${inputs.pythonDir} does not exist on disk. Skipping Python cache save because no managed Python installation was found. If you want uv to install managed Python instead of using a system interpreter, set UV_PYTHON_PREFERENCE=only-managed.`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const pythonCacheKey = `${cacheKey}-python`;
|
|
await saveCacheToKey(
|
|
pythonCacheKey,
|
|
inputs.pythonDir,
|
|
STATE_PYTHON_CACHE_MATCHED_KEY,
|
|
"Python cache",
|
|
);
|
|
}
|
|
}
|
|
|
|
async function pruneCache(): Promise<void> {
|
|
const forceSupported = pep440.gte(core.getState(STATE_UV_VERSION), "0.8.24");
|
|
|
|
const options: exec.ExecOptions = {
|
|
silent: false,
|
|
};
|
|
const execArgs = ["cache", "prune", "--ci"];
|
|
if (forceSupported) {
|
|
execArgs.push("--force");
|
|
}
|
|
|
|
log.info("Pruning cache...");
|
|
const uvPath = core.getState(STATE_UV_PATH);
|
|
await exec.exec(uvPath, execArgs, options);
|
|
}
|
|
|
|
function getUvCachePath(inputs: SetupInputs): string {
|
|
if (inputs.cacheLocalPath === undefined) {
|
|
throw new Error(
|
|
"cache-local-path is not set. Cannot save cache without a valid cache path.",
|
|
);
|
|
}
|
|
if (
|
|
process.env.UV_CACHE_DIR &&
|
|
process.env.UV_CACHE_DIR !== inputs.cacheLocalPath.path
|
|
) {
|
|
log.warning(
|
|
`The environment variable UV_CACHE_DIR has been changed to "${process.env.UV_CACHE_DIR}", by an action or step running after astral-sh/setup-uv. This can lead to unexpected behavior. If you expected this to happen set the cache-local-path input to "${process.env.UV_CACHE_DIR}" instead of "${inputs.cacheLocalPath.path}".`,
|
|
);
|
|
return process.env.UV_CACHE_DIR;
|
|
}
|
|
return inputs.cacheLocalPath.path;
|
|
}
|
|
|
|
async function saveCacheToKey(
|
|
cacheKey: string,
|
|
cachePath: string,
|
|
stateKey: string,
|
|
cacheName: string,
|
|
): Promise<void> {
|
|
const matchedKey = core.getState(stateKey);
|
|
|
|
if (matchedKey === cacheKey) {
|
|
log.info(`${cacheName} hit occurred on key ${cacheKey}, not saving cache.`);
|
|
return;
|
|
}
|
|
|
|
log.info(`Including ${cacheName} path: ${cachePath}`);
|
|
const cacheId = await cache.saveCache([cachePath], cacheKey);
|
|
if (cacheId !== -1) {
|
|
log.info(`${cacheName} saved with key: ${cacheKey}`);
|
|
}
|
|
}
|
|
|
|
run();
|