mirror of
https://github.com/actions/setup-go.git
synced 2026-03-14 22:55:45 +01:00
fix: allow multiple invocations with caching enabled
This fix addresses the issue where calling setup-go multiple times with
caching enabled in the same workflow would fail because the second
invocation attempted to save to the same cache key.
Changes:
- Add tracking of processed cache keys using state variables to prevent
duplicate cache save attempts
- Add helper functions in constants.ts for state management:
- getAlreadyCachedKey()/setAlreadyCachedKey(): Track keys already in cache
- getPrimaryCacheKey()/setPrimaryCacheKey(): Track the primary key for
each invocation
- getCachedGoModPath()/setCachedGoModPath(): Track which go.mod was cached
- Modify cache-restore.ts to store state about the cache operation
- Modify cache-save.ts to check if cache was already saved for this
go.mod path before attempting to save again
- Add comprehensive tests for the multiple invocation scenario
This enables workflows that need to setup Go with different configurations
(e.g., different working directories) multiple times without cache
conflicts.
Assisted-By: cagent
This commit is contained in:
parent
27fdb267c1
commit
49fe0b8fcc
7 changed files with 642 additions and 31 deletions
|
|
@ -6,6 +6,7 @@ import fs from 'fs';
|
|||
import * as cacheRestore from '../src/cache-restore';
|
||||
import * as cacheUtils from '../src/cache-utils';
|
||||
import {PackageManagerInfo} from '../src/package-managers';
|
||||
import {State} from '../src/constants';
|
||||
|
||||
describe('restoreCache', () => {
|
||||
let hashFilesSpy: jest.SpyInstance;
|
||||
|
|
@ -13,22 +14,34 @@ describe('restoreCache', () => {
|
|||
let restoreCacheSpy: jest.SpyInstance;
|
||||
let infoSpy: jest.SpyInstance;
|
||||
let setOutputSpy: jest.SpyInstance;
|
||||
let saveStateSpy: jest.SpyInstance;
|
||||
let getStateSpy: jest.SpyInstance;
|
||||
|
||||
const versionSpec = '1.13.1';
|
||||
const packageManager = 'default';
|
||||
const cacheDependencyPath = 'path';
|
||||
|
||||
let originalWorkspace: string | undefined;
|
||||
let stateStore: Record<string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
originalWorkspace = process.env.GITHUB_WORKSPACE;
|
||||
process.env.GITHUB_WORKSPACE = '/test/workspace';
|
||||
//Arrange
|
||||
stateStore = {};
|
||||
|
||||
hashFilesSpy = jest.spyOn(glob, 'hashFiles');
|
||||
getCacheDirectoryPathSpy = jest.spyOn(cacheUtils, 'getCacheDirectoryPath');
|
||||
restoreCacheSpy = jest.spyOn(cache, 'restoreCache');
|
||||
infoSpy = jest.spyOn(core, 'info');
|
||||
setOutputSpy = jest.spyOn(core, 'setOutput');
|
||||
saveStateSpy = jest
|
||||
.spyOn(core, 'saveState')
|
||||
.mockImplementation((key, value) => {
|
||||
stateStore[key] = value as string;
|
||||
});
|
||||
getStateSpy = jest.spyOn(core, 'getState').mockImplementation(key => {
|
||||
return stateStore[key] || '';
|
||||
});
|
||||
|
||||
getCacheDirectoryPathSpy.mockImplementation(
|
||||
(PackageManager: PackageManagerInfo) => {
|
||||
|
|
@ -46,9 +59,7 @@ describe('restoreCache', () => {
|
|||
});
|
||||
|
||||
it('should throw if dependency file path is not valid', async () => {
|
||||
// Arrange
|
||||
hashFilesSpy.mockImplementation(() => Promise.resolve(''));
|
||||
// Act + Assert
|
||||
await expect(
|
||||
cacheRestore.restoreCache(
|
||||
versionSpec,
|
||||
|
|
@ -61,10 +72,8 @@ describe('restoreCache', () => {
|
|||
});
|
||||
|
||||
it('should inform if cache hit is not occurred', async () => {
|
||||
// Arrange
|
||||
hashFilesSpy.mockImplementation(() => Promise.resolve('file_hash'));
|
||||
restoreCacheSpy.mockImplementation(() => Promise.resolve(''));
|
||||
// Act + Assert
|
||||
await cacheRestore.restoreCache(
|
||||
versionSpec,
|
||||
packageManager,
|
||||
|
|
@ -74,10 +83,8 @@ describe('restoreCache', () => {
|
|||
});
|
||||
|
||||
it('should set output if cache hit is occurred', async () => {
|
||||
// Arrange
|
||||
hashFilesSpy.mockImplementation(() => Promise.resolve('file_hash'));
|
||||
restoreCacheSpy.mockImplementation(() => Promise.resolve('cache_key'));
|
||||
// Act + Assert
|
||||
await cacheRestore.restoreCache(
|
||||
versionSpec,
|
||||
packageManager,
|
||||
|
|
@ -90,13 +97,125 @@ describe('restoreCache', () => {
|
|||
jest.spyOn(fs, 'readdirSync').mockReturnValue(['main.go'] as any);
|
||||
|
||||
await expect(
|
||||
cacheRestore.restoreCache(
|
||||
versionSpec,
|
||||
packageManager
|
||||
// No cacheDependencyPath
|
||||
)
|
||||
cacheRestore.restoreCache(versionSpec, packageManager)
|
||||
).rejects.toThrow(
|
||||
'Dependencies file is not found in /test/workspace. Supported file pattern: go.mod'
|
||||
);
|
||||
});
|
||||
|
||||
describe('multiple invocations', () => {
|
||||
it('should skip restore if same key was already processed', async () => {
|
||||
hashFilesSpy.mockImplementation(() => Promise.resolve('file_hash'));
|
||||
restoreCacheSpy.mockImplementation(() => Promise.resolve('cache_key'));
|
||||
|
||||
// First invocation
|
||||
await cacheRestore.restoreCache(
|
||||
versionSpec,
|
||||
packageManager,
|
||||
cacheDependencyPath
|
||||
);
|
||||
expect(restoreCacheSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second invocation with same parameters should skip
|
||||
await cacheRestore.restoreCache(
|
||||
versionSpec,
|
||||
packageManager,
|
||||
cacheDependencyPath
|
||||
);
|
||||
|
||||
// restoreCache should not be called again
|
||||
expect(restoreCacheSpy).toHaveBeenCalledTimes(1);
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('already processed in this job')
|
||||
);
|
||||
});
|
||||
|
||||
it('should restore cache for different versions', async () => {
|
||||
hashFilesSpy.mockImplementation(() => Promise.resolve('file_hash'));
|
||||
restoreCacheSpy.mockImplementation(() => Promise.resolve('cache_key'));
|
||||
|
||||
// First invocation with version 1.13.1
|
||||
await cacheRestore.restoreCache(
|
||||
'1.13.1',
|
||||
packageManager,
|
||||
cacheDependencyPath
|
||||
);
|
||||
expect(restoreCacheSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second invocation with different version
|
||||
await cacheRestore.restoreCache(
|
||||
'1.20.0',
|
||||
packageManager,
|
||||
cacheDependencyPath
|
||||
);
|
||||
|
||||
// Both should call restoreCache
|
||||
expect(restoreCacheSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should accumulate primary keys for multiple invocations', async () => {
|
||||
hashFilesSpy.mockImplementation(() => Promise.resolve('file_hash'));
|
||||
restoreCacheSpy.mockImplementation(() => Promise.resolve(''));
|
||||
|
||||
await cacheRestore.restoreCache(
|
||||
'1.13.1',
|
||||
packageManager,
|
||||
cacheDependencyPath
|
||||
);
|
||||
await cacheRestore.restoreCache(
|
||||
'1.20.0',
|
||||
packageManager,
|
||||
cacheDependencyPath
|
||||
);
|
||||
|
||||
// Check that CachePrimaryKeys state contains both keys
|
||||
const keysJson = stateStore[State.CachePrimaryKeys];
|
||||
expect(keysJson).toBeDefined();
|
||||
const keys = JSON.parse(keysJson);
|
||||
expect(keys).toHaveLength(2);
|
||||
expect(keys[0]).toContain('go-1.13.1');
|
||||
expect(keys[1]).toContain('go-1.20.0');
|
||||
});
|
||||
|
||||
it('should accumulate matched keys for cache hits', async () => {
|
||||
hashFilesSpy.mockImplementation(() => Promise.resolve('file_hash'));
|
||||
restoreCacheSpy
|
||||
.mockImplementationOnce(() => Promise.resolve('cache_key_1'))
|
||||
.mockImplementationOnce(() => Promise.resolve('cache_key_2'));
|
||||
|
||||
await cacheRestore.restoreCache(
|
||||
'1.13.1',
|
||||
packageManager,
|
||||
cacheDependencyPath
|
||||
);
|
||||
await cacheRestore.restoreCache(
|
||||
'1.20.0',
|
||||
packageManager,
|
||||
cacheDependencyPath
|
||||
);
|
||||
|
||||
// Check that CacheMatchedKeys state contains both matched keys
|
||||
const keysJson = stateStore[State.CacheMatchedKeys];
|
||||
expect(keysJson).toBeDefined();
|
||||
const keys = JSON.parse(keysJson);
|
||||
expect(keys).toHaveLength(2);
|
||||
expect(keys).toContain('cache_key_1');
|
||||
expect(keys).toContain('cache_key_2');
|
||||
});
|
||||
|
||||
it('should maintain backward compatibility with legacy state keys', async () => {
|
||||
hashFilesSpy.mockImplementation(() => Promise.resolve('file_hash'));
|
||||
restoreCacheSpy.mockImplementation(() => Promise.resolve('cache_key'));
|
||||
|
||||
await cacheRestore.restoreCache(
|
||||
versionSpec,
|
||||
packageManager,
|
||||
cacheDependencyPath
|
||||
);
|
||||
|
||||
// Legacy keys should still be set
|
||||
expect(stateStore[State.CachePrimaryKey]).toBeDefined();
|
||||
expect(stateStore[State.CacheMatchedKey]).toBe('cache_key');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
220
__tests__/cache-save.test.ts
Normal file
220
__tests__/cache-save.test.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import * as cache from '@actions/cache';
|
||||
import * as core from '@actions/core';
|
||||
import fs from 'fs';
|
||||
|
||||
import * as cacheSave from '../src/cache-save';
|
||||
import * as cacheUtils from '../src/cache-utils';
|
||||
import {PackageManagerInfo} from '../src/package-managers';
|
||||
import {State} from '../src/constants';
|
||||
|
||||
describe('cache-save', () => {
|
||||
let getCacheDirectoryPathSpy: jest.SpyInstance;
|
||||
let saveCacheSpy: jest.SpyInstance;
|
||||
let infoSpy: jest.SpyInstance;
|
||||
let warningSpy: jest.SpyInstance;
|
||||
let getBooleanInputSpy: jest.SpyInstance;
|
||||
let getStateSpy: jest.SpyInstance;
|
||||
let existsSyncSpy: jest.SpyInstance;
|
||||
|
||||
let stateStore: Record<string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
stateStore = {};
|
||||
|
||||
getCacheDirectoryPathSpy = jest.spyOn(cacheUtils, 'getCacheDirectoryPath');
|
||||
saveCacheSpy = jest.spyOn(cache, 'saveCache');
|
||||
infoSpy = jest.spyOn(core, 'info').mockImplementation();
|
||||
warningSpy = jest.spyOn(core, 'warning').mockImplementation();
|
||||
getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
|
||||
getStateSpy = jest.spyOn(core, 'getState').mockImplementation(key => {
|
||||
return stateStore[key] || '';
|
||||
});
|
||||
existsSyncSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(true);
|
||||
|
||||
getCacheDirectoryPathSpy.mockImplementation(
|
||||
(PackageManager: PackageManagerInfo) => {
|
||||
return Promise.resolve(['/home/runner/go/pkg/mod']);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('run', () => {
|
||||
it('should skip cache save when cache input is false', async () => {
|
||||
getBooleanInputSpy.mockReturnValue(false);
|
||||
|
||||
await cacheSave.run(false);
|
||||
|
||||
expect(saveCacheSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save cache with legacy single key', async () => {
|
||||
getBooleanInputSpy.mockReturnValue(true);
|
||||
stateStore[State.CachePrimaryKey] = 'primary-key-123';
|
||||
stateStore[State.CacheMatchedKey] = '';
|
||||
saveCacheSpy.mockResolvedValue(12345);
|
||||
|
||||
await cacheSave.run(false);
|
||||
|
||||
expect(saveCacheSpy).toHaveBeenCalledTimes(1);
|
||||
expect(saveCacheSpy).toHaveBeenCalledWith(
|
||||
['/home/runner/go/pkg/mod'],
|
||||
'primary-key-123'
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip save when cache hit occurred (legacy mode)', async () => {
|
||||
getBooleanInputSpy.mockReturnValue(true);
|
||||
stateStore[State.CachePrimaryKey] = 'primary-key-123';
|
||||
stateStore[State.CacheMatchedKey] = 'primary-key-123';
|
||||
|
||||
await cacheSave.run(false);
|
||||
|
||||
expect(saveCacheSpy).not.toHaveBeenCalled();
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Cache hit occurred on the primary key')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple invocations', () => {
|
||||
it('should save cache for multiple keys from multiple invocations', async () => {
|
||||
getBooleanInputSpy.mockReturnValue(true);
|
||||
stateStore[State.CachePrimaryKeys] = JSON.stringify([
|
||||
'key-go-1.13.1',
|
||||
'key-go-1.20.0'
|
||||
]);
|
||||
stateStore[State.CacheMatchedKeys] = JSON.stringify(['', '']);
|
||||
saveCacheSpy.mockResolvedValue(12345);
|
||||
|
||||
await cacheSave.run(false);
|
||||
|
||||
expect(saveCacheSpy).toHaveBeenCalledTimes(2);
|
||||
expect(saveCacheSpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
['/home/runner/go/pkg/mod'],
|
||||
'key-go-1.13.1'
|
||||
);
|
||||
expect(saveCacheSpy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
['/home/runner/go/pkg/mod'],
|
||||
'key-go-1.20.0'
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip save for keys that had cache hits', async () => {
|
||||
getBooleanInputSpy.mockReturnValue(true);
|
||||
stateStore[State.CachePrimaryKeys] = JSON.stringify([
|
||||
'key-go-1.13.1',
|
||||
'key-go-1.20.0'
|
||||
]);
|
||||
// First key had a cache hit, second didn't
|
||||
stateStore[State.CacheMatchedKeys] = JSON.stringify([
|
||||
'key-go-1.13.1',
|
||||
''
|
||||
]);
|
||||
saveCacheSpy.mockResolvedValue(12345);
|
||||
|
||||
await cacheSave.run(false);
|
||||
|
||||
// Should only save for the second key
|
||||
expect(saveCacheSpy).toHaveBeenCalledTimes(1);
|
||||
expect(saveCacheSpy).toHaveBeenCalledWith(
|
||||
['/home/runner/go/pkg/mod'],
|
||||
'key-go-1.20.0'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle cache already exists error gracefully', async () => {
|
||||
getBooleanInputSpy.mockReturnValue(true);
|
||||
stateStore[State.CachePrimaryKeys] = JSON.stringify([
|
||||
'key-go-1.13.1',
|
||||
'key-go-1.20.0'
|
||||
]);
|
||||
stateStore[State.CacheMatchedKeys] = JSON.stringify(['', '']);
|
||||
|
||||
saveCacheSpy
|
||||
.mockRejectedValueOnce(new Error('Cache already exists'))
|
||||
.mockResolvedValueOnce(12345);
|
||||
|
||||
await cacheSave.run(false);
|
||||
|
||||
expect(saveCacheSpy).toHaveBeenCalledTimes(2);
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Cache already exists')
|
||||
);
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Cache saved with the key: key-go-1.20.0')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty state gracefully', async () => {
|
||||
getBooleanInputSpy.mockReturnValue(true);
|
||||
// No state set
|
||||
|
||||
await cacheSave.run(false);
|
||||
|
||||
expect(saveCacheSpy).not.toHaveBeenCalled();
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Primary key was not generated')
|
||||
);
|
||||
});
|
||||
|
||||
it('should prefer multi-key state over legacy single-key state', async () => {
|
||||
getBooleanInputSpy.mockReturnValue(true);
|
||||
// Both legacy and multi-key state present
|
||||
stateStore[State.CachePrimaryKey] = 'legacy-key';
|
||||
stateStore[State.CacheMatchedKey] = '';
|
||||
stateStore[State.CachePrimaryKeys] = JSON.stringify(['multi-key-1']);
|
||||
stateStore[State.CacheMatchedKeys] = JSON.stringify(['']);
|
||||
saveCacheSpy.mockResolvedValue(12345);
|
||||
|
||||
await cacheSave.run(false);
|
||||
|
||||
// Should use multi-key state
|
||||
expect(saveCacheSpy).toHaveBeenCalledTimes(1);
|
||||
expect(saveCacheSpy).toHaveBeenCalledWith(
|
||||
['/home/runner/go/pkg/mod'],
|
||||
'multi-key-1'
|
||||
);
|
||||
});
|
||||
|
||||
it('should log summary for multiple invocations', async () => {
|
||||
getBooleanInputSpy.mockReturnValue(true);
|
||||
stateStore[State.CachePrimaryKeys] = JSON.stringify([
|
||||
'key-go-1.13.1',
|
||||
'key-go-1.20.0',
|
||||
'key-go-1.21.0'
|
||||
]);
|
||||
// First had cache hit, second and third didn't
|
||||
stateStore[State.CacheMatchedKeys] = JSON.stringify([
|
||||
'key-go-1.13.1',
|
||||
'',
|
||||
''
|
||||
]);
|
||||
saveCacheSpy.mockResolvedValue(12345);
|
||||
|
||||
await cacheSave.run(false);
|
||||
|
||||
expect(saveCacheSpy).toHaveBeenCalledTimes(2);
|
||||
expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining('Saved: 2'));
|
||||
});
|
||||
|
||||
it('should warn when cache folder does not exist', async () => {
|
||||
getBooleanInputSpy.mockReturnValue(true);
|
||||
stateStore[State.CachePrimaryKeys] = JSON.stringify(['key-go-1.13.1']);
|
||||
stateStore[State.CacheMatchedKeys] = JSON.stringify(['']);
|
||||
existsSyncSpy.mockReturnValue(false);
|
||||
|
||||
await cacheSave.run(false);
|
||||
|
||||
expect(warningSpy).toHaveBeenCalledWith(
|
||||
'There are no cache folders on the disk'
|
||||
);
|
||||
expect(saveCacheSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
99
dist/cache-save/index.js
vendored
99
dist/cache-save/index.js
vendored
|
|
@ -71570,8 +71570,6 @@ function run(earlyExit) {
|
|||
}
|
||||
const cachePackages = () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
const packageManager = 'default';
|
||||
const state = core.getState(constants_1.State.CacheMatchedKey);
|
||||
const primaryKey = core.getState(constants_1.State.CachePrimaryKey);
|
||||
const packageManagerInfo = yield (0, cache_utils_1.getPackageManagerInfo)(packageManager);
|
||||
const cachePaths = yield (0, cache_utils_1.getCacheDirectoryPath)(packageManagerInfo);
|
||||
const nonExistingPaths = cachePaths.filter(cachePath => !fs_1.default.existsSync(cachePath));
|
||||
|
|
@ -71582,20 +71580,96 @@ const cachePackages = () => __awaiter(void 0, void 0, void 0, function* () {
|
|||
if (nonExistingPaths.length) {
|
||||
logWarning(`Cache folder path is retrieved but doesn't exist on disk: ${nonExistingPaths.join(', ')}`);
|
||||
}
|
||||
if (!primaryKey) {
|
||||
core.info('Primary key was not generated. Please check the log messages above for more errors or information');
|
||||
// Get all primary keys and matched keys from multiple invocations
|
||||
const primaryKeys = getPrimaryKeys();
|
||||
const matchedKeys = getMatchedKeys();
|
||||
if (primaryKeys.length === 0) {
|
||||
// Fallback to legacy single-key behavior
|
||||
const primaryKey = core.getState(constants_1.State.CachePrimaryKey);
|
||||
const matchedKey = core.getState(constants_1.State.CacheMatchedKey);
|
||||
if (primaryKey) {
|
||||
yield saveSingleCache(cachePaths, primaryKey, matchedKey);
|
||||
}
|
||||
else {
|
||||
core.info('Primary key was not generated. Please check the log messages above for more errors or information');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (primaryKey === state) {
|
||||
core.info(`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`);
|
||||
return;
|
||||
// Process each primary key from multiple invocations
|
||||
let savedCount = 0;
|
||||
let skippedCount = 0;
|
||||
for (let i = 0; i < primaryKeys.length; i++) {
|
||||
const primaryKey = primaryKeys[i];
|
||||
const matchedKey = matchedKeys[i] || '';
|
||||
if (primaryKey === matchedKey) {
|
||||
core.info(`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const cacheId = yield cache.saveCache(cachePaths, primaryKey);
|
||||
if (cacheId === -1) {
|
||||
core.info(`Cache save returned -1 for key: ${primaryKey}`);
|
||||
continue;
|
||||
}
|
||||
core.info(`Cache saved with the key: ${primaryKey}`);
|
||||
savedCount++;
|
||||
}
|
||||
catch (error) {
|
||||
// If save fails (e.g., cache already exists), log and continue
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage.includes('Cache already exists')) {
|
||||
core.info(`Cache already exists for key: ${primaryKey}`);
|
||||
skippedCount++;
|
||||
}
|
||||
else {
|
||||
logWarning(`Failed to save cache for key ${primaryKey}: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const cacheId = yield cache.saveCache(cachePaths, primaryKey);
|
||||
if (cacheId === -1) {
|
||||
return;
|
||||
if (savedCount > 0 || skippedCount > 0) {
|
||||
core.info(`Cache save complete. Saved: ${savedCount}, Skipped (already cached): ${skippedCount}`);
|
||||
}
|
||||
core.info(`Cache saved with the key: ${primaryKey}`);
|
||||
});
|
||||
function saveSingleCache(cachePaths, primaryKey, matchedKey) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (!primaryKey) {
|
||||
core.info('Primary key was not generated. Please check the log messages above for more errors or information');
|
||||
return;
|
||||
}
|
||||
if (primaryKey === matchedKey) {
|
||||
core.info(`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`);
|
||||
return;
|
||||
}
|
||||
const cacheId = yield cache.saveCache(cachePaths, primaryKey);
|
||||
if (cacheId === -1) {
|
||||
return;
|
||||
}
|
||||
core.info(`Cache saved with the key: ${primaryKey}`);
|
||||
});
|
||||
}
|
||||
function getPrimaryKeys() {
|
||||
try {
|
||||
const keysJson = core.getState(constants_1.State.CachePrimaryKeys);
|
||||
if (!keysJson)
|
||||
return [];
|
||||
return JSON.parse(keysJson);
|
||||
}
|
||||
catch (_a) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function getMatchedKeys() {
|
||||
try {
|
||||
const keysJson = core.getState(constants_1.State.CacheMatchedKeys);
|
||||
if (!keysJson)
|
||||
return [];
|
||||
return JSON.parse(keysJson);
|
||||
}
|
||||
catch (_a) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function logWarning(message) {
|
||||
const warningPrefix = '[warning]';
|
||||
core.info(`${warningPrefix}${message}`);
|
||||
|
|
@ -71731,6 +71805,9 @@ var State;
|
|||
(function (State) {
|
||||
State["CachePrimaryKey"] = "CACHE_KEY";
|
||||
State["CacheMatchedKey"] = "CACHE_RESULT";
|
||||
// For multiple invocations support - stores JSON arrays of keys
|
||||
State["CachePrimaryKeys"] = "CACHE_KEYS";
|
||||
State["CacheMatchedKeys"] = "CACHE_RESULTS";
|
||||
})(State || (exports.State = State = {}));
|
||||
var Outputs;
|
||||
(function (Outputs) {
|
||||
|
|
|
|||
53
dist/setup/index.js
vendored
53
dist/setup/index.js
vendored
|
|
@ -76820,6 +76820,16 @@ const restoreCache = (versionSpec, packageManager, cacheDependencyPath) => __awa
|
|||
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}`);
|
||||
// Check if this key was already processed in a previous invocation
|
||||
const existingKeys = getExistingPrimaryKeys();
|
||||
if (existingKeys.includes(primaryKey)) {
|
||||
core.info(`Cache key ${primaryKey} already processed in this job, skipping restore`);
|
||||
core.setOutput(constants_1.Outputs.CacheHit, true);
|
||||
return;
|
||||
}
|
||||
// Save state for post step - accumulate keys for multiple invocations
|
||||
addPrimaryKey(primaryKey);
|
||||
// Legacy single-key state (for backward compatibility)
|
||||
core.saveState(constants_1.State.CachePrimaryKey, primaryKey);
|
||||
const cacheKey = yield cache.restoreCache(cachePaths, primaryKey);
|
||||
core.setOutput(constants_1.Outputs.CacheHit, Boolean(cacheKey));
|
||||
|
|
@ -76828,6 +76838,9 @@ const restoreCache = (versionSpec, packageManager, cacheDependencyPath) => __awa
|
|||
core.setOutput(constants_1.Outputs.CacheHit, false);
|
||||
return;
|
||||
}
|
||||
// Save matched key state - accumulate for multiple invocations
|
||||
addMatchedKey(cacheKey);
|
||||
// Legacy single-key state (for backward compatibility)
|
||||
core.saveState(constants_1.State.CacheMatchedKey, cacheKey);
|
||||
core.info(`Cache restored from key: ${cacheKey}`);
|
||||
});
|
||||
|
|
@ -76842,6 +76855,43 @@ const findDependencyFile = (packageManager) => {
|
|||
}
|
||||
return path_1.default.join(workspace, dependencyFile);
|
||||
};
|
||||
// Helper functions for managing multiple cache keys
|
||||
function getExistingPrimaryKeys() {
|
||||
try {
|
||||
const keysJson = core.getState(constants_1.State.CachePrimaryKeys);
|
||||
if (!keysJson)
|
||||
return [];
|
||||
return JSON.parse(keysJson);
|
||||
}
|
||||
catch (_a) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function addPrimaryKey(key) {
|
||||
const existingKeys = getExistingPrimaryKeys();
|
||||
if (!existingKeys.includes(key)) {
|
||||
existingKeys.push(key);
|
||||
core.saveState(constants_1.State.CachePrimaryKeys, JSON.stringify(existingKeys));
|
||||
}
|
||||
}
|
||||
function getExistingMatchedKeys() {
|
||||
try {
|
||||
const keysJson = core.getState(constants_1.State.CacheMatchedKeys);
|
||||
if (!keysJson)
|
||||
return [];
|
||||
return JSON.parse(keysJson);
|
||||
}
|
||||
catch (_a) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function addMatchedKey(key) {
|
||||
const existingKeys = getExistingMatchedKeys();
|
||||
if (!existingKeys.includes(key)) {
|
||||
existingKeys.push(key);
|
||||
core.saveState(constants_1.State.CacheMatchedKeys, JSON.stringify(existingKeys));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
|
@ -76972,6 +77022,9 @@ var State;
|
|||
(function (State) {
|
||||
State["CachePrimaryKey"] = "CACHE_KEY";
|
||||
State["CacheMatchedKey"] = "CACHE_RESULT";
|
||||
// For multiple invocations support - stores JSON arrays of keys
|
||||
State["CachePrimaryKeys"] = "CACHE_KEYS";
|
||||
State["CacheMatchedKeys"] = "CACHE_RESULTS";
|
||||
})(State || (exports.State = State = {}));
|
||||
var Outputs;
|
||||
(function (Outputs) {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,20 @@ export const restoreCache = async (
|
|||
const primaryKey = `setup-go-${platform}-${arch}-${linuxVersion}go-${versionSpec}-${fileHash}`;
|
||||
core.debug(`primary key is ${primaryKey}`);
|
||||
|
||||
// Check if this key was already processed in a previous invocation
|
||||
const existingKeys = getExistingPrimaryKeys();
|
||||
if (existingKeys.includes(primaryKey)) {
|
||||
core.info(
|
||||
`Cache key ${primaryKey} already processed in this job, skipping restore`
|
||||
);
|
||||
core.setOutput(Outputs.CacheHit, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save state for post step - accumulate keys for multiple invocations
|
||||
addPrimaryKey(primaryKey);
|
||||
|
||||
// Legacy single-key state (for backward compatibility)
|
||||
core.saveState(State.CachePrimaryKey, primaryKey);
|
||||
|
||||
const cacheKey = await cache.restoreCache(cachePaths, primaryKey);
|
||||
|
|
@ -46,6 +60,10 @@ export const restoreCache = async (
|
|||
return;
|
||||
}
|
||||
|
||||
// Save matched key state - accumulate for multiple invocations
|
||||
addMatchedKey(cacheKey);
|
||||
|
||||
// Legacy single-key state (for backward compatibility)
|
||||
core.saveState(State.CacheMatchedKey, cacheKey);
|
||||
core.info(`Cache restored from key: ${cacheKey}`);
|
||||
};
|
||||
|
|
@ -64,3 +82,40 @@ const findDependencyFile = (packageManager: PackageManagerInfo) => {
|
|||
|
||||
return path.join(workspace, dependencyFile);
|
||||
};
|
||||
|
||||
// Helper functions for managing multiple cache keys
|
||||
function getExistingPrimaryKeys(): string[] {
|
||||
try {
|
||||
const keysJson = core.getState(State.CachePrimaryKeys);
|
||||
if (!keysJson) return [];
|
||||
return JSON.parse(keysJson) as string[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function addPrimaryKey(key: string): void {
|
||||
const existingKeys = getExistingPrimaryKeys();
|
||||
if (!existingKeys.includes(key)) {
|
||||
existingKeys.push(key);
|
||||
core.saveState(State.CachePrimaryKeys, JSON.stringify(existingKeys));
|
||||
}
|
||||
}
|
||||
|
||||
function getExistingMatchedKeys(): string[] {
|
||||
try {
|
||||
const keysJson = core.getState(State.CacheMatchedKeys);
|
||||
if (!keysJson) return [];
|
||||
return JSON.parse(keysJson) as string[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function addMatchedKey(key: string): void {
|
||||
const existingKeys = getExistingMatchedKeys();
|
||||
if (!existingKeys.includes(key)) {
|
||||
existingKeys.push(key);
|
||||
core.saveState(State.CacheMatchedKeys, JSON.stringify(existingKeys));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,12 +40,7 @@ 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(
|
||||
|
|
@ -65,6 +60,75 @@ const cachePackages = async () => {
|
|||
);
|
||||
}
|
||||
|
||||
// Get all primary keys and matched keys from multiple invocations
|
||||
const primaryKeys = getPrimaryKeys();
|
||||
const matchedKeys = getMatchedKeys();
|
||||
|
||||
if (primaryKeys.length === 0) {
|
||||
// Fallback to legacy single-key behavior
|
||||
const primaryKey = core.getState(State.CachePrimaryKey);
|
||||
const matchedKey = core.getState(State.CacheMatchedKey);
|
||||
if (primaryKey) {
|
||||
await saveSingleCache(cachePaths, primaryKey, matchedKey);
|
||||
} else {
|
||||
core.info(
|
||||
'Primary key was not generated. Please check the log messages above for more errors or information'
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each primary key from multiple invocations
|
||||
let savedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (let i = 0; i < primaryKeys.length; i++) {
|
||||
const primaryKey = primaryKeys[i];
|
||||
const matchedKey = matchedKeys[i] || '';
|
||||
|
||||
if (primaryKey === matchedKey) {
|
||||
core.info(
|
||||
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
||||
);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const cacheId = await cache.saveCache(cachePaths, primaryKey);
|
||||
if (cacheId === -1) {
|
||||
core.info(`Cache save returned -1 for key: ${primaryKey}`);
|
||||
continue;
|
||||
}
|
||||
core.info(`Cache saved with the key: ${primaryKey}`);
|
||||
savedCount++;
|
||||
} catch (error) {
|
||||
// If save fails (e.g., cache already exists), log and continue
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage.includes('Cache already exists')) {
|
||||
core.info(`Cache already exists for key: ${primaryKey}`);
|
||||
skippedCount++;
|
||||
} else {
|
||||
logWarning(
|
||||
`Failed to save cache for key ${primaryKey}: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (savedCount > 0 || skippedCount > 0) {
|
||||
core.info(
|
||||
`Cache save complete. Saved: ${savedCount}, Skipped (already cached): ${skippedCount}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
async function saveSingleCache(
|
||||
cachePaths: string[],
|
||||
primaryKey: string,
|
||||
matchedKey: string
|
||||
): Promise<void> {
|
||||
if (!primaryKey) {
|
||||
core.info(
|
||||
'Primary key was not generated. Please check the log messages above for more errors or information'
|
||||
|
|
@ -72,7 +136,7 @@ const cachePackages = async () => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (primaryKey === state) {
|
||||
if (primaryKey === matchedKey) {
|
||||
core.info(
|
||||
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
||||
);
|
||||
|
|
@ -84,7 +148,27 @@ const cachePackages = async () => {
|
|||
return;
|
||||
}
|
||||
core.info(`Cache saved with the key: ${primaryKey}`);
|
||||
};
|
||||
}
|
||||
|
||||
function getPrimaryKeys(): string[] {
|
||||
try {
|
||||
const keysJson = core.getState(State.CachePrimaryKeys);
|
||||
if (!keysJson) return [];
|
||||
return JSON.parse(keysJson) as string[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getMatchedKeys(): string[] {
|
||||
try {
|
||||
const keysJson = core.getState(State.CacheMatchedKeys);
|
||||
if (!keysJson) return [];
|
||||
return JSON.parse(keysJson) as string[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function logWarning(message: string): void {
|
||||
const warningPrefix = '[warning]';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
export enum State {
|
||||
CachePrimaryKey = 'CACHE_KEY',
|
||||
CacheMatchedKey = 'CACHE_RESULT'
|
||||
CacheMatchedKey = 'CACHE_RESULT',
|
||||
// For multiple invocations support - stores JSON arrays of keys
|
||||
CachePrimaryKeys = 'CACHE_KEYS',
|
||||
CacheMatchedKeys = 'CACHE_RESULTS'
|
||||
}
|
||||
|
||||
export enum Outputs {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue