mirror of
https://github.com/actions/setup-go.git
synced 2026-10-07 11:53:00 +00:00
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:
1 parent
90ad2b35f6
commit
ad9941188f
13 files changed
+1035
-335
No files matched your search
+48
-13
@@ -6,13 +6,19 @@ import fs from 'fs';
|
||||
|
||||
import {State, Outputs} from './constants.js';
|
||||
import {PackageManagerInfo} from './package-managers.js';
|
||||
import {getCacheDirectoryPath, getPackageManagerInfo} from './cache-utils.js';
|
||||
import {
|
||||
CacheEntry,
|
||||
canRestoreCachesInParallel,
|
||||
getCacheDirectoryPath,
|
||||
getPackageManagerInfo
|
||||
} from './cache-utils.js';
|
||||
|
||||
export const restoreCache = async (
|
||||
versionSpec: string,
|
||||
packageManager: string,
|
||||
cacheDependencyPath?: string
|
||||
) => {
|
||||
core.setOutput(Outputs.CacheHit, false);
|
||||
const packageManagerInfo = await getPackageManagerInfo(packageManager);
|
||||
const platform = process.env.RUNNER_OS;
|
||||
const arch = process.arch;
|
||||
@@ -32,22 +38,51 @@ export const restoreCache = async (
|
||||
|
||||
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}`);
|
||||
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: CacheEntry[] = cachePaths.map(directory => ({
|
||||
...directory,
|
||||
primaryKey: primaryKeys[directory.kind]
|
||||
}));
|
||||
|
||||
core.saveState(State.CachePrimaryKey, primaryKey);
|
||||
const restoreEntry = async (entry: CacheEntry) => {
|
||||
core.debug(`${entry.kind} primary key is ${entry.primaryKey}`);
|
||||
try {
|
||||
entry.matchedKey = await cache.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);
|
||||
core.warning(`Restore ${entry.kind} cache failed: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const cacheKey = await cache.restoreCache(cachePaths, primaryKey);
|
||||
core.setOutput(Outputs.CacheHit, Boolean(cacheKey));
|
||||
|
||||
if (!cacheKey) {
|
||||
core.info(`Cache is not found`);
|
||||
core.setOutput(Outputs.CacheHit, false);
|
||||
return;
|
||||
if (canRestoreCachesInParallel(entries)) {
|
||||
await Promise.all(entries.map(restoreEntry));
|
||||
} else {
|
||||
for (const entry of entries) {
|
||||
await restoreEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
core.saveState(State.CacheMatchedKey, cacheKey);
|
||||
core.info(`Cache restored from key: ${cacheKey}`);
|
||||
// Preserve the resolved paths so the post step uses the same cache versions,
|
||||
// even if a later step changes the Go environment.
|
||||
core.saveState(State.CacheEntries, entries);
|
||||
core.setOutput(
|
||||
Outputs.CacheHit,
|
||||
entries.length === packageManagerInfo.cacheFolderCommandList.length &&
|
||||
entries.every(entry => entry.matchedKey === entry.primaryKey)
|
||||
);
|
||||
};
|
||||
|
||||
const findDependencyFile = (packageManager: PackageManagerInfo) => {
|
||||
|
||||
+26
-30
@@ -2,7 +2,7 @@ import * as core from '@actions/core';
|
||||
import * as cache from '@actions/cache';
|
||||
import fs from 'fs';
|
||||
import {State} from './constants.js';
|
||||
import {getCacheDirectoryPath, getPackageManagerInfo} from './cache-utils.js';
|
||||
import type {CacheEntry} from './cache-utils.js';
|
||||
|
||||
// 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
|
||||
@@ -39,47 +39,43 @@ export async function run(earlyExit?: boolean) {
|
||||
}
|
||||
|
||||
const cachePackages = async () => {
|
||||
const packageManager = 'default';
|
||||
|
||||
const state = core.getState(State.CacheMatchedKey);
|
||||
const primaryKey = core.getState(State.CachePrimaryKey);
|
||||
|
||||
const packageManagerInfo = await getPackageManagerInfo(packageManager);
|
||||
|
||||
const cachePaths = await getCacheDirectoryPath(packageManagerInfo);
|
||||
|
||||
const nonExistingPaths = cachePaths.filter(
|
||||
cachePath => !fs.existsSync(cachePath)
|
||||
const entries: CacheEntry[] = JSON.parse(
|
||||
core.getState(State.CacheEntries) || '[]'
|
||||
);
|
||||
|
||||
if (nonExistingPaths.length === cachePaths.length) {
|
||||
core.warning('There are no cache folders on the disk');
|
||||
return;
|
||||
}
|
||||
|
||||
if (nonExistingPaths.length) {
|
||||
logWarning(
|
||||
`Cache folder path is retrieved but doesn't exist on disk: ${nonExistingPaths.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!primaryKey) {
|
||||
if (!entries.length) {
|
||||
core.info(
|
||||
'Primary key was not generated. Please check the log messages above for more errors or information'
|
||||
'Cache keys were 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);
|
||||
core.warning(`Save ${entry.kind} cache failed: ${message}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const saveCacheEntry = async (entry: CacheEntry) => {
|
||||
const {kind, path, primaryKey, matchedKey} = entry;
|
||||
if (primaryKey === matchedKey) {
|
||||
core.info(
|
||||
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheId = await cache.saveCache(cachePaths, primaryKey);
|
||||
if (!fs.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
|
||||
|
||||
+88
-12
@@ -1,11 +1,24 @@
|
||||
import * as cache from '@actions/cache';
|
||||
import * as core from '@actions/core';
|
||||
import * as exec from '@actions/exec';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {
|
||||
supportedPackageManagers,
|
||||
PackageManagerInfo
|
||||
PackageManagerInfo,
|
||||
CacheKind
|
||||
} from './package-managers.js';
|
||||
|
||||
export interface CacheDirectory {
|
||||
kind: CacheKind;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface CacheEntry extends CacheDirectory {
|
||||
primaryKey: string;
|
||||
matchedKey?: string;
|
||||
}
|
||||
|
||||
export const getCommandOutput = async (toolCommand: string) => {
|
||||
let {stdout, stderr, exitCode} = await exec.getExecOutput(
|
||||
toolCommand,
|
||||
@@ -36,24 +49,24 @@ export const getPackageManagerInfo = async (packageManager: string) => {
|
||||
|
||||
export const getCacheDirectoryPath = async (
|
||||
packageManagerInfo: PackageManagerInfo
|
||||
) => {
|
||||
): Promise<CacheDirectory[]> => {
|
||||
const pathOutputs = await Promise.allSettled(
|
||||
packageManagerInfo.cacheFolderCommandList.map(async command =>
|
||||
getCommandOutput(command)
|
||||
)
|
||||
packageManagerInfo.cacheFolderCommandList.map(async ({kind, command}) => ({
|
||||
kind,
|
||||
path: await getCommandOutput(command)
|
||||
}))
|
||||
);
|
||||
|
||||
const results = pathOutputs.map(item => {
|
||||
const cachePaths: CacheDirectory[] = [];
|
||||
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.`);
|
||||
@@ -62,6 +75,69 @@ export const getCacheDirectoryPath = async (
|
||||
return cachePaths;
|
||||
};
|
||||
|
||||
export function canRestoreCachesInParallel(
|
||||
directories: CacheDirectory[]
|
||||
): boolean {
|
||||
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 = path.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 = path.relative(parent, child);
|
||||
return (
|
||||
relative === '..' ||
|
||||
relative.startsWith(`..${path.sep}`) ||
|
||||
path.isAbsolute(relative)
|
||||
);
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
// If the paths cannot be resolved safely, avoid concurrent extraction.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCachePath(directory: string): string {
|
||||
const absolutePath = path.resolve(directory);
|
||||
try {
|
||||
fs.lstatSync(absolutePath);
|
||||
} catch (error) {
|
||||
const parent = path.dirname(absolutePath);
|
||||
if (
|
||||
(error as NodeJS.ErrnoException).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 path.join(resolveCachePath(parent), path.basename(absolutePath));
|
||||
}
|
||||
return fs.realpathSync(absolutePath);
|
||||
}
|
||||
|
||||
export function isGhes(): boolean {
|
||||
const ghUrl = new URL(
|
||||
process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
export enum State {
|
||||
CachePrimaryKey = 'CACHE_KEY',
|
||||
CacheMatchedKey = 'CACHE_RESULT'
|
||||
CacheEntries = 'CACHE_ENTRIES'
|
||||
}
|
||||
|
||||
export enum Outputs {
|
||||
|
||||
@@ -2,14 +2,19 @@ type SupportedPackageManagers = {
|
||||
[prop: string]: PackageManagerInfo;
|
||||
};
|
||||
|
||||
export type CacheKind = 'modules' | 'build';
|
||||
|
||||
export interface PackageManagerInfo {
|
||||
dependencyFilePattern: string;
|
||||
cacheFolderCommandList: string[];
|
||||
cacheFolderCommandList: {kind: CacheKind; command: string}[];
|
||||
}
|
||||
|
||||
export const supportedPackageManagers: 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'}
|
||||
]
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user