From f01118b9a577a6f633cef9618c8987b1882b26f9 Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Wed, 13 Mar 2024 21:15:09 +0100 Subject: [PATCH] Cache fallback with rolling time based expiration --- README.md | 2 ++ __tests__/cache-restore.test.ts | 10 +++++----- action.yml | 2 ++ dist/setup/index.js | 21 +++++++++++++++------ src/cache-restore.ts | 19 ++++++++++++++----- src/main.ts | 3 ++- 6 files changed, 40 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 86236a49..cd1858dc 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,8 @@ The action defaults to search for the dependency file (`package-lock.json`, `npm **Note:** The action does not cache `node_modules` +Use `cache-invalidate-after-days` to change the default fallback cache invalidation of every 120 days. Set to 0 to deactivate. + See the examples of using cache for `yarn`/`pnpm` and `cache-dependency-path` input in the [Advanced usage](docs/advanced-usage.md#caching-packages-data) guide. **Caching npm dependencies:** diff --git a/__tests__/cache-restore.test.ts b/__tests__/cache-restore.test.ts index 90153a40..135c9f87 100644 --- a/__tests__/cache-restore.test.ts +++ b/__tests__/cache-restore.test.ts @@ -132,13 +132,13 @@ describe('cache-restore', () => { } }); - await restoreCache(packageManager, ''); + await restoreCache(packageManager, '', '0'); expect(hashFilesSpy).toHaveBeenCalled(); expect(infoSpy).toHaveBeenCalledWith( - `Cache restored from key: node-cache-${platform}-${packageManager}-${fileHash}` + `Cache restored from key: ${platform}-0-setup-node-${packageManager}-${fileHash}` ); expect(infoSpy).not.toHaveBeenCalledWith( - `${packageManager} cache is not found` + `Cache not found for input keys: ${platform}-0-setup-node-${packageManager}-${fileHash}, ${platform}-0-setup-node-${packageManager}-` ); expect(setOutputSpy).toHaveBeenCalledWith('cache-hit', true); } @@ -163,10 +163,10 @@ describe('cache-restore', () => { }); restoreCacheSpy.mockImplementationOnce(() => undefined); - await restoreCache(packageManager, ''); + await restoreCache(packageManager, '', '0'); expect(hashFilesSpy).toHaveBeenCalled(); expect(infoSpy).toHaveBeenCalledWith( - `${packageManager} cache is not found` + `Cache not found for input keys: ${platform}-0-setup-node-${packageManager}-${fileHash}, ${platform}-0-setup-node-${packageManager}-` ); expect(setOutputSpy).toHaveBeenCalledWith('cache-hit', false); } diff --git a/action.yml b/action.yml index 99db5869..eca0ab50 100644 --- a/action.yml +++ b/action.yml @@ -25,6 +25,8 @@ inputs: description: 'Used to specify a package manager for caching in the default directory. Supported values: npm, yarn, pnpm.' cache-dependency-path: description: 'Used to specify the path to a dependency file: package-lock.json, yarn.lock, etc. Supports wildcards or a list of file names for caching multiple dependencies.' + cache-invalidate-after-days: + description: 'Used to control how often the fallback cache is invalidated automatically.' # TODO: add input to control forcing to pull from cloud or dist. # escape valve for someone having issues or needing the absolute latest which isn't cached yet outputs: diff --git a/dist/setup/index.js b/dist/setup/index.js index 1f4334d3..29196140 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -93090,7 +93090,7 @@ const path_1 = __importDefault(__nccwpck_require__(1017)); const fs_1 = __importDefault(__nccwpck_require__(7147)); const constants_1 = __nccwpck_require__(9042); const cache_utils_1 = __nccwpck_require__(1678); -const restoreCache = (packageManager, cacheDependencyPath) => __awaiter(void 0, void 0, void 0, function* () { +const restoreCache = (packageManager, cacheDependencyPath, cacheInvalidateAfterDays) => __awaiter(void 0, void 0, void 0, function* () { const packageManagerInfo = yield (0, cache_utils_1.getPackageManagerInfo)(packageManager); if (!packageManagerInfo) { throw new Error(`Caching for '${packageManager}' is not supported`); @@ -93105,22 +93105,30 @@ const restoreCache = (packageManager, cacheDependencyPath) => __awaiter(void 0, if (!fileHash) { throw new Error('Some specified paths were not resolved, unable to cache dependencies.'); } - const keyPrefix = `node-cache-${platform}-${packageManager}`; + const numericCacheInvalidateAfterDays = cacheInvalidateAfterDays && cacheInvalidateAfterDays === '0' + ? 0 + : (parseInt(cacheInvalidateAfterDays || '', 10) || 120); + const timedInvalidationPrefix = numericCacheInvalidateAfterDays + ? Math.floor(Date.now() / (1000 * 60 * 60 * 24 * numericCacheInvalidateAfterDays)) % 1000 // % 1000 to get a rolling prefix between 0 and 999 rather than a possibly infinitely large + : 0; + const keyPrefixBase = `node-cache-${platform}-${packageManager}`; + const keyPrefix = `${keyPrefixBase}-${timedInvalidationPrefix}`; const primaryKey = `${keyPrefix}-${fileHash}`; + const restoreKeys = [`${keyPrefix}-`]; core.debug(`primary key is ${primaryKey}`); core.saveState(constants_1.State.CachePrimaryKey, primaryKey); const isManagedByYarnBerry = yield (0, cache_utils_1.repoHasYarnBerryManagedDependencies)(packageManagerInfo, cacheDependencyPath); let cacheKey; if (isManagedByYarnBerry) { core.info('All dependencies are managed locally by yarn3, the previous cache can be used'); - cacheKey = yield cache.restoreCache(cachePaths, primaryKey, [keyPrefix]); + cacheKey = yield cache.restoreCache(cachePaths, primaryKey, [keyPrefixBase]); } else { - cacheKey = yield cache.restoreCache(cachePaths, primaryKey); + cacheKey = yield cache.restoreCache(cachePaths, primaryKey, restoreKeys); } core.setOutput('cache-hit', Boolean(cacheKey)); if (!cacheKey) { - core.info(`${packageManager} cache is not found`); + core.info(`Cache not found for input keys: ${[primaryKey, ...restoreKeys].join(', ')}`); return; } core.saveState(constants_1.State.CacheMatchedKey, cacheKey); @@ -94251,7 +94259,8 @@ function run() { if (cache && (0, cache_utils_1.isCacheFeatureAvailable)()) { core.saveState(constants_1.State.CachePackageManager, cache); const cacheDependencyPath = core.getInput('cache-dependency-path'); - yield (0, cache_restore_1.restoreCache)(cache, cacheDependencyPath); + const cacheInvalidateAfterDays = core.getInput('cache-invalidate-after-days'); + yield (0, cache_restore_1.restoreCache)(cache, cacheDependencyPath, cacheInvalidateAfterDays); } const matchersPath = path.join(__dirname, '../..', '.github'); core.info(`##[add-matcher]${path.join(matchersPath, 'tsc.json')}`); diff --git a/src/cache-restore.ts b/src/cache-restore.ts index 3b230970..deb912e5 100644 --- a/src/cache-restore.ts +++ b/src/cache-restore.ts @@ -14,7 +14,8 @@ import { export const restoreCache = async ( packageManager: string, - cacheDependencyPath: string + cacheDependencyPath: string, + cacheInvalidateAfterDays?: string ) => { const packageManagerInfo = await getPackageManagerInfo(packageManager); if (!packageManagerInfo) { @@ -37,9 +38,17 @@ export const restoreCache = async ( 'Some specified paths were not resolved, unable to cache dependencies.' ); } + const numericCacheInvalidateAfterDays = cacheInvalidateAfterDays && cacheInvalidateAfterDays === '0' + ? 0 + : (parseInt(cacheInvalidateAfterDays || '', 10) || 120) + const timedInvalidationPrefix = numericCacheInvalidateAfterDays + ? Math.floor(Date.now() / (1000 * 60 * 60 * 24 * numericCacheInvalidateAfterDays)) % 1000 // % 1000 to get a rolling prefix between 0 and 999 rather than a possibly infinitely large + : 0; - const keyPrefix = `node-cache-${platform}-${packageManager}`; + const keyPrefixBase = `node-cache-${platform}-${packageManager}`; + const keyPrefix = `${keyPrefixBase}-${timedInvalidationPrefix}`; const primaryKey = `${keyPrefix}-${fileHash}`; + const restoreKeys = [`${keyPrefix}-`]; core.debug(`primary key is ${primaryKey}`); core.saveState(State.CachePrimaryKey, primaryKey); @@ -53,15 +62,15 @@ export const restoreCache = async ( core.info( 'All dependencies are managed locally by yarn3, the previous cache can be used' ); - cacheKey = await cache.restoreCache(cachePaths, primaryKey, [keyPrefix]); + cacheKey = await cache.restoreCache(cachePaths, primaryKey, [keyPrefixBase]); } else { - cacheKey = await cache.restoreCache(cachePaths, primaryKey); + cacheKey = await cache.restoreCache(cachePaths, primaryKey, restoreKeys); } core.setOutput('cache-hit', Boolean(cacheKey)); if (!cacheKey) { - core.info(`${packageManager} cache is not found`); + core.info(`Cache not found for input keys: ${[primaryKey, ...restoreKeys].join(', ')}`); return; } diff --git a/src/main.ts b/src/main.ts index c55c3b00..f3adfc06 100644 --- a/src/main.ts +++ b/src/main.ts @@ -62,7 +62,8 @@ export async function run() { if (cache && isCacheFeatureAvailable()) { core.saveState(State.CachePackageManager, cache); const cacheDependencyPath = core.getInput('cache-dependency-path'); - await restoreCache(cache, cacheDependencyPath); + const cacheInvalidateAfterDays = core.getInput('cache-invalidate-after-days'); + await restoreCache(cache, cacheDependencyPath, cacheInvalidateAfterDays); } const matchersPath = path.join(__dirname, '../..', '.github');