feat: split Go module and build caches

Reuse downloaded modules across Go versions, runner architectures, and
runner images on the same OS instead of duplicating them in each build
cache archive.

Restore and save each entry independently, running operations in parallel
when safe. Serialize restores for overlapping or aliased paths, preserve
the original paths for the post step, and report cache-hit only when both
entries match their primary keys.

Update the documented manual restore keys and generated action bundles.
This commit is contained in:
qmuntal
2026-10-05 15:05:38 +02:00
parent 90ad2b35f6
commit ad9941188f
13 changed files with 1035 additions and 335 deletions

View File

@@ -40669,8 +40669,8 @@ function getExecOutput(commandLine, args, options) {
let stdout = '';
let stderr = '';
//Using string decoder covers the case where a mult-byte character is split
const stdoutDecoder = new external_string_decoder_.StringDecoder('utf8');
const stderrDecoder = new external_string_decoder_.StringDecoder('utf8');
const stdoutDecoder = new StringDecoder('utf8');
const stderrDecoder = new StringDecoder('utf8');
const originalStdoutListener = (_a = options === null || options === void 0 ? void 0 : options.listeners) === null || _a === void 0 ? void 0 : _a.stdout;
const originalStdErrListener = (_b = options === null || options === void 0 ? void 0 : options.listeners) === null || _b === void 0 ? void 0 : _b.stderr;
const stdErrListener = (data) => {
@@ -96210,8 +96210,7 @@ function saveCacheV2(paths_1, key_1, options_1) {
;// CONCATENATED MODULE: ./src/constants.ts
var State;
(function (State) {
State["CachePrimaryKey"] = "CACHE_KEY";
State["CacheMatchedKey"] = "CACHE_RESULT";
State["CacheEntries"] = "CACHE_ENTRIES";
})(State || (State = {}));
var Outputs;
(function (Outputs) {
@@ -96229,79 +96228,11 @@ const GO_ENV_OUTPUTS = (/* unused pure expression or super */ null && ([
'GOTOOLDIR'
]));
;// CONCATENATED MODULE: ./src/package-managers.ts
const supportedPackageManagers = {
default: {
dependencyFilePattern: 'go.mod',
cacheFolderCommandList: ['go env GOMODCACHE', 'go env GOCACHE']
}
};
;// CONCATENATED MODULE: ./src/cache-utils.ts
const getCommandOutput = async (toolCommand) => {
let { stdout, stderr, exitCode } = await getExecOutput(toolCommand, undefined, { ignoreReturnCode: true });
if (exitCode) {
stderr = !stderr.trim()
? `The '${toolCommand}' command failed with exit code: ${exitCode}`
: stderr;
throw new Error(stderr);
}
return stdout.trim();
};
const getPackageManagerInfo = async (packageManager) => {
if (!supportedPackageManagers[packageManager]) {
throw new Error(`It's not possible to use ${packageManager}, please, check correctness of the package manager name spelling.`);
}
const obtainedPackageManager = supportedPackageManagers[packageManager];
return obtainedPackageManager;
};
const getCacheDirectoryPath = async (packageManagerInfo) => {
const pathOutputs = await Promise.allSettled(packageManagerInfo.cacheFolderCommandList.map(async (command) => getCommandOutput(command)));
const results = pathOutputs.map(item => {
if (item.status === 'fulfilled') {
return item.value;
}
else {
info(`[warning]getting cache directory path failed: ${item.reason}`);
}
return '';
});
const cachePaths = results.filter(item => item);
if (!cachePaths.length) {
throw new Error(`Could not get cache folder paths.`);
}
return cachePaths;
};
function cache_utils_isGhes() {
const ghUrl = new URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com');
const hostname = ghUrl.hostname.trimEnd().toUpperCase();
const isGitHubHost = hostname === 'GITHUB.COM';
const isGitHubEnterpriseCloudHost = hostname.endsWith('.GHE.COM');
const isLocalHost = hostname.endsWith('.LOCALHOST');
return !isGitHubHost && !isGitHubEnterpriseCloudHost && !isLocalHost;
}
function isCacheFeatureAvailable() {
if (cache.isFeatureAvailable()) {
return true;
}
if (cache_utils_isGhes()) {
core.warning('Cache action is only supported on GHES version >= 3.5. If you are on version >=3.5 Please check with GHES admin if Actions cache service is enabled or not.');
return false;
}
core.warning('The runner was not able to contact the cache service. Caching will be skipped');
return false;
}
;// CONCATENATED MODULE: ./src/cache-save.ts
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
// throw an uncaught exception. Instead of failing this action, just warn.
@@ -96334,28 +96265,32 @@ async function run(earlyExit) {
}
}
const cachePackages = async () => {
const packageManager = 'default';
const state = getState(State.CacheMatchedKey);
const primaryKey = getState(State.CachePrimaryKey);
const packageManagerInfo = await getPackageManagerInfo(packageManager);
const cachePaths = await getCacheDirectoryPath(packageManagerInfo);
const nonExistingPaths = cachePaths.filter(cachePath => !external_fs_default().existsSync(cachePath));
if (nonExistingPaths.length === cachePaths.length) {
warning('There are no cache folders on the disk');
const entries = JSON.parse(getState(State.CacheEntries) || '[]');
if (!entries.length) {
info('Cache keys were not generated. Please check the log messages above for more errors or information');
return;
}
if (nonExistingPaths.length) {
logWarning(`Cache folder path is retrieved but doesn't exist on disk: ${nonExistingPaths.join(', ')}`);
}
if (!primaryKey) {
info('Primary key was not generated. Please check the log messages above for more errors or information');
return;
}
if (primaryKey === state) {
await Promise.all(entries.map(async (entry) => {
try {
await saveCacheEntry(entry);
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
warning(`Save ${entry.kind} cache failed: ${message}`);
}
}));
};
const saveCacheEntry = async (entry) => {
const { kind, path, primaryKey, matchedKey } = entry;
if (primaryKey === matchedKey) {
info(`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`);
return;
}
const cacheId = await cache_saveCache(cachePaths, primaryKey);
if (!external_fs_default().existsSync(path)) {
logWarning(`${kind} cache folder doesn't exist on disk: ${path}`);
return;
}
const cacheId = await cache_saveCache([path], primaryKey);
if (cacheId === -1) {
// saveCache returns -1 without throwing when the cache was not saved, e.g.
// a reserve collision or a read-only token (fork PR). @actions/cache has

128
dist/setup/index.js vendored
View File

@@ -97543,8 +97543,7 @@ function saveCacheV2(paths_1, key_1, options_1) {
;// CONCATENATED MODULE: ./src/constants.ts
var State;
(function (State) {
State["CachePrimaryKey"] = "CACHE_KEY";
State["CacheMatchedKey"] = "CACHE_RESULT";
State["CacheEntries"] = "CACHE_ENTRIES";
})(State || (State = {}));
var Outputs;
(function (Outputs) {
@@ -97566,7 +97565,10 @@ const GO_ENV_OUTPUTS = [
const supportedPackageManagers = {
default: {
dependencyFilePattern: 'go.mod',
cacheFolderCommandList: ['go env GOMODCACHE', 'go env GOCACHE']
cacheFolderCommandList: [
{ kind: 'modules', command: 'go env GOMODCACHE' },
{ kind: 'build', command: 'go env GOCACHE' }
]
}
};
@@ -97575,6 +97577,8 @@ const supportedPackageManagers = {
const getCommandOutput = async (toolCommand) => {
let { stdout, stderr, exitCode } = await getExecOutput(toolCommand, undefined, { ignoreReturnCode: true });
if (exitCode) {
@@ -97593,22 +97597,78 @@ const getPackageManagerInfo = async (packageManager) => {
return obtainedPackageManager;
};
const getCacheDirectoryPath = async (packageManagerInfo) => {
const pathOutputs = await Promise.allSettled(packageManagerInfo.cacheFolderCommandList.map(async (command) => getCommandOutput(command)));
const results = pathOutputs.map(item => {
const pathOutputs = await Promise.allSettled(packageManagerInfo.cacheFolderCommandList.map(async ({ kind, command }) => ({
kind,
path: await getCommandOutput(command)
})));
const cachePaths = [];
for (const item of pathOutputs) {
if (item.status === 'fulfilled') {
return item.value;
if (item.value.path) {
cachePaths.push(item.value);
}
}
else {
core_info(`[warning]getting cache directory path failed: ${item.reason}`);
}
return '';
});
const cachePaths = results.filter(item => item);
}
if (!cachePaths.length) {
throw new Error(`Could not get cache folder paths.`);
}
return cachePaths;
};
function canRestoreCachesInParallel(directories) {
try {
const paths = directories.map(directory => {
const resolvedPath = resolveCachePath(directory.path);
// Conservatively handle case-insensitive Windows/macOS filesystems.
return process.platform === 'darwin' || process.platform === 'win32'
? resolvedPath.toLowerCase()
: resolvedPath;
});
// A cache archive can replace symlinks traversed by the other restore.
// Canonical destinations alone do not capture that extraction footprint.
if (paths.some((resolvedPath, index) => {
const absolutePath = external_path_default().resolve(directories[index].path);
const normalizedPath = process.platform === 'darwin' || process.platform === 'win32'
? absolutePath.toLowerCase()
: absolutePath;
return resolvedPath !== normalizedPath;
})) {
return false;
}
return paths.every((parent, index) => paths.every((child, otherIndex) => {
if (index === otherIndex) {
return true;
}
const relative = external_path_default().relative(parent, child);
return (relative === '..' ||
relative.startsWith(`..${(external_path_default()).sep}`) ||
external_path_default().isAbsolute(relative));
}));
}
catch {
// If the paths cannot be resolved safely, avoid concurrent extraction.
return false;
}
}
function resolveCachePath(directory) {
const absolutePath = external_path_default().resolve(directory);
try {
external_fs_default().lstatSync(absolutePath);
}
catch (error) {
const parent = external_path_default().dirname(absolutePath);
if (error.code !== 'ENOENT' ||
parent === absolutePath) {
throw error;
}
// Cold cache directories may not exist yet. Resolve their existing
// ancestors so symlink aliases still participate in the overlap check.
return external_path_default().join(resolveCachePath(parent), external_path_default().basename(absolutePath));
}
return external_fs_default().realpathSync(absolutePath);
}
function cache_utils_isGhes() {
const ghUrl = new URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com');
const hostname = ghUrl.hostname.trimEnd().toUpperCase();
@@ -97638,6 +97698,7 @@ function isCacheFeatureAvailable() {
const cache_restore_restoreCache = async (versionSpec, packageManager, cacheDependencyPath) => {
setOutput(Outputs.CacheHit, false);
const packageManagerInfo = await getPackageManagerInfo(packageManager);
const platform = process.env.RUNNER_OS;
const arch = process.arch;
@@ -97650,18 +97711,45 @@ const cache_restore_restoreCache = async (versionSpec, packageManager, cacheDepe
throw new Error('Some specified paths were not resolved, unable to cache dependencies.');
}
const linuxVersion = process.env.RUNNER_OS === 'Linux' ? `${process.env.ImageOS}-` : '';
const primaryKey = `setup-go-${platform}-${arch}-${linuxVersion}go-${versionSpec}-${fileHash}`;
core_debug(`primary key is ${primaryKey}`);
saveState(State.CachePrimaryKey, primaryKey);
const cacheKey = await restoreCache(cachePaths, primaryKey);
setOutput(Outputs.CacheHit, Boolean(cacheKey));
if (!cacheKey) {
core_info(`Cache is not found`);
setOutput(Outputs.CacheHit, false);
return;
const primaryKeys = {
// Downloaded module sources can be reused across Go versions and runner
// architectures. Keep OS isolation for filesystem compatibility.
modules: `setup-go-modules-${platform}-${fileHash}`,
build: `setup-go-build-${platform}-${arch}-${linuxVersion}go-${versionSpec}-${fileHash}`
};
const entries = cachePaths.map(directory => ({
...directory,
primaryKey: primaryKeys[directory.kind]
}));
const restoreEntry = async (entry) => {
core_debug(`${entry.kind} primary key is ${entry.primaryKey}`);
try {
entry.matchedKey = await restoreCache([entry.path], entry.primaryKey);
if (entry.matchedKey) {
core_info(`${entry.kind} cache restored from key: ${entry.matchedKey}`);
}
else {
core_info(`${entry.kind} cache is not found`);
}
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
warning(`Restore ${entry.kind} cache failed: ${message}`);
}
};
if (canRestoreCachesInParallel(entries)) {
await Promise.all(entries.map(restoreEntry));
}
saveState(State.CacheMatchedKey, cacheKey);
core_info(`Cache restored from key: ${cacheKey}`);
else {
for (const entry of entries) {
await restoreEntry(entry);
}
}
// Preserve the resolved paths so the post step uses the same cache versions,
// even if a later step changes the Go environment.
saveState(State.CacheEntries, entries);
setOutput(Outputs.CacheHit, entries.length === packageManagerInfo.cacheFolderCommandList.length &&
entries.every(entry => entry.matchedKey === entry.primaryKey));
};
const findDependencyFile = (packageManager) => {
const dependencyFile = packageManager.dependencyFilePattern;