Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cache fallback with rolling timed expiration #702

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down
10 changes: 5 additions & 5 deletions __tests__/cache-restore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 15 additions & 6 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand All @@ -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);
Expand Down Expand Up @@ -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')}`);
Expand Down
19 changes: 14 additions & 5 deletions src/cache-restore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {

export const restoreCache = async (
packageManager: string,
cacheDependencyPath: string
cacheDependencyPath: string,
cacheInvalidateAfterDays?: string
) => {
const packageManagerInfo = await getPackageManagerInfo(packageManager);
if (!packageManagerInfo) {
Expand All @@ -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);
Expand All @@ -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;
}

Expand Down
3 changes: 2 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down