Compare commits

..

6 Commits

Author SHA1 Message Date
Gregorio Litenstein
7755eb5907
Update tests for deleteCacheByKey.
Signed-off-by: Gregorio Litenstein <g.litenstein@gmail.com>
2025-02-03 15:20:15 -03:00
Gregorio Litenstein
69f9e5f5c7
Update some dependencies.
Signed-off-by: Gregorio Litenstein <g.litenstein@gmail.com>
2025-02-03 15:20:14 -03:00
Gregorio Litenstein
cd2c83758f
Don’t accidentally deleting cache from base branch
Signed-off-by: Gregorio Litenstein <g.litenstein@gmail.com>
2025-02-03 15:20:14 -03:00
Gregorio Litenstein
daa04cb8a7
Allow refreshing cache also with granular save. 2025-02-03 15:20:14 -03:00
Gregorio Litenstein
64c069ba53
cache-refresh: update documentation and licenses 2025-02-03 15:20:14 -03:00
Gregorio Litenstein
95a38ca0bf
Add tests for cache refreshing. 2025-02-03 15:20:13 -03:00
12 changed files with 296540 additions and 231639 deletions

View File

@ -1,6 +1,5 @@
import * as cache from "@actions/cache"; import * as cache from "@actions/cache";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { RequestError } from "@octokit/request-error";
import nock from "nock"; import nock from "nock";
import { Events, RefKey } from "../src/constants"; import { Events, RefKey } from "../src/constants";
@ -208,7 +207,7 @@ test("getInputAsBool throws if required and value missing", () => {
).toThrowError(); ).toThrowError();
}); });
test("deleteCacheByKey returns 'HttpError: 404' when cache is not found.", async () => { test("deleteCacheByKey produces 'HttpError: 404' when cache is not found.", async () => {
const event = Events.Push; const event = Events.Push;
process.env["GITHUB_REPOSITORY"] = "owner/repo"; process.env["GITHUB_REPOSITORY"] = "owner/repo";
@ -226,14 +225,73 @@ test("deleteCacheByKey returns 'HttpError: 404' when cache is not found.", async
expect(logWarningMock).toHaveBeenCalledWith( expect(logWarningMock).toHaveBeenCalledWith(
expect.stringMatching(/404: Not Found/i) expect.stringMatching(/404: Not Found/i)
); );
expect(response).toBeInstanceOf(RequestError); expect(response).toBe(undefined);
expect(response).toMatchObject({
name: "HttpError",
status: 404
});
}); });
test("deleteCacheByKey returns 'HttpError: 401' on an invalid non-mocked request.", async () => { test("deleteCacheByKey does not delete anything if it finds more than one entry for the given key.", async () => {
const event = Events.Push;
process.env["GITHUB_REPOSITORY"] = "owner/repo";
process.env["GITHUB_TOKEN"] =
"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";
process.env["GITHUB_ACTION"] = "__owner___run-repo";
process.env[Events.Key] = event;
process.env[RefKey] = "";
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const response = await actionUtils.deleteCacheByKey(
testUtils.failureCacheKey,
"owner",
"repo"
);
expect(logWarningMock).toHaveBeenCalledWith(
`More than one cache entry found for key ${testUtils.failureCacheKey}`
);
expect(response).toBe(undefined);
});
test("deleteCacheByKey does not delete anything if the key matches a cache belonging to another ref.", async () => {
const event = Events.Push;
process.env["GITHUB_REPOSITORY"] = "owner/repo";
process.env["GITHUB_TOKEN"] =
"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";
process.env["GITHUB_ACTION"] = "__owner___run-repo";
process.env[Events.Key] = event;
process.env[RefKey] = "ref/heads/feature";
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const response = await actionUtils.deleteCacheByKey(
testUtils.wrongRefCacheKey,
"owner",
"repo"
);
expect(logWarningMock).toHaveBeenCalledWith(
`No cache entries for key ${testUtils.wrongRefCacheKey} belong to gitref ${process.env[RefKey]}.`
);
expect(response).toBe(undefined);
});
test("deleteCacheByKey produces 'HttpError: 404' when cache is not found.", async () => {
const event = Events.Push;
process.env["GITHUB_REPOSITORY"] = "owner/repo";
process.env["GITHUB_TOKEN"] =
"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";
process.env["GITHUB_ACTION"] = "__owner___run-repo";
process.env[Events.Key] = event;
process.env[RefKey] = "ref/heads/feature";
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const response = await actionUtils.deleteCacheByKey(
testUtils.failureCacheKey,
"owner",
"repo"
);
expect(logWarningMock).toHaveBeenCalledWith(
expect.stringMatching(/404: Not Found/i)
);
expect(response).toBe(undefined);
});
test("deleteCacheByKey produces 'HttpError: 401' on an invalid non-mocked request.", async () => {
const event = Events.Push; const event = Events.Push;
process.env["GITHUB_REPOSITORY"] = "owner/repo"; process.env["GITHUB_REPOSITORY"] = "owner/repo";
@ -252,15 +310,11 @@ test("deleteCacheByKey returns 'HttpError: 401' on an invalid non-mocked request
expect(logWarningMock).toHaveBeenCalledWith( expect(logWarningMock).toHaveBeenCalledWith(
expect.stringMatching(/401: Bad Credentials/i) expect.stringMatching(/401: Bad Credentials/i)
); );
expect(response).toBeInstanceOf(RequestError); expect(response).toBe(undefined);
expect(response).toMatchObject({
name: "HttpError",
status: 401
});
nock.disableNetConnect(); nock.disableNetConnect();
}); });
test("deleteCacheByKey returns matched cache data when successful.", async () => { test("deleteCacheByKey returns 204 / No Content when successful.", async () => {
const event = Events.Push; const event = Events.Push;
process.env["GITHUB_REPOSITORY"] = "owner/repo"; process.env["GITHUB_REPOSITORY"] = "owner/repo";
@ -270,29 +324,13 @@ test("deleteCacheByKey returns matched cache data when successful.", async () =>
process.env[Events.Key] = event; process.env[Events.Key] = event;
process.env[RefKey] = "ref/heads/feature"; process.env[RefKey] = "ref/heads/feature";
const expectedResponse = {
id: expect.any(Number),
ref: expect.any(String),
key: expect.any(String),
version: expect.any(String),
last_accessed_at: expect.any(String),
created_at: expect.any(String),
size_in_bytes: expect.any(Number)
};
const logWarningMock = jest.spyOn(actionUtils, "logWarning"); const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const response = await actionUtils.deleteCacheByKey( const response = await actionUtils.deleteCacheByKey(
testUtils.successCacheKey, testUtils.successCacheKey,
"owner", "owner",
"repo" "repo"
); );
expect(response).toMatchObject({ expect(response).toBe(204);
data: expect.objectContaining({
total_count: expect.any(Number),
actions_caches: expect.arrayContaining([
expect.objectContaining(expectedResponse)
])
})
});
expect(logWarningMock).toHaveBeenCalledTimes(0); expect(logWarningMock).toHaveBeenCalledTimes(0);
}); });

View File

@ -492,7 +492,11 @@ test("save with cache hit and refresh-cache will try to delete and re-create ent
); );
expect(infoMock).toHaveBeenNthCalledWith( expect(infoMock).toHaveBeenNthCalledWith(
2, 2,
`Succesfully deleted cache with key: ${primaryKey}` expect.stringMatching(
new RegExp(
`Succesfully deleted cache with key: ${primaryKey}, id: \\d+`
)
)
); );
expect(infoMock).toHaveBeenNthCalledWith( expect(infoMock).toHaveBeenNthCalledWith(
3, 3,
@ -565,7 +569,11 @@ test("Granular save will use lookup to determine if cache needs to be updated or
); );
expect(infoMock).toHaveBeenNthCalledWith( expect(infoMock).toHaveBeenNthCalledWith(
2, 2,
`Succesfully deleted cache with key: ${primaryKey}` expect.stringMatching(
new RegExp(
`Succesfully deleted cache with key: ${primaryKey}, id: \\d+`
)
)
); );
expect(infoMock).toHaveBeenNthCalledWith( expect(infoMock).toHaveBeenNthCalledWith(
3, 3,

130273
dist/restore-only/index.js vendored

File diff suppressed because one or more lines are too long

130291
dist/restore/index.js vendored

File diff suppressed because one or more lines are too long

129976
dist/save-only/index.js vendored

File diff suppressed because one or more lines are too long

129926
dist/save/index.js vendored

File diff suppressed because one or more lines are too long

6873
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -32,10 +32,10 @@
"devDependencies": { "devDependencies": {
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/nock": "^11.1.0", "@types/nock": "^11.1.0",
"@types/node": "^16.18.3", "@types/node": "^20.14.8",
"@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0", "@typescript-eslint/parser": "^5.45.0",
"@zeit/ncc": "^0.20.5", "@vercel/ncc": "^0.38.3",
"eslint": "^8.28.0", "eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
@ -44,13 +44,10 @@
"eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-simple-import-sort": "^7.0.0",
"jest": "^28.1.3", "jest": "^28.1.3",
"jest-circus": "^27.5.1", "jest-circus": "^27.5.1",
"msw": "^0.49.3", "msw": "^1.3.5",
"nock": "^13.2.9", "nock": "^13.2.9",
"prettier": "^2.8.0", "prettier": "^2.8.0",
"ts-jest": "^28.0.8", "ts-jest": "^28.0.8",
"typescript": "^4.9.3" "typescript": "^4.9.5"
},
"overrides": {
"@mswjs/interceptors": "^0.17.7"
} }
} }

View File

@ -63,6 +63,7 @@ async function saveImpl(stateProvider: IStateProvider): Promise<number | void> {
); );
} }
if (utils.isExactKeyMatch(primaryKey, restoredKey)) { if (utils.isExactKeyMatch(primaryKey, restoredKey)) {
/* istanbul ignore next */
const { GITHUB_TOKEN, GITHUB_REPOSITORY } = process.env || null; const { GITHUB_TOKEN, GITHUB_REPOSITORY } = process.env || null;
if (GITHUB_TOKEN && GITHUB_REPOSITORY && refreshCache === true) { if (GITHUB_TOKEN && GITHUB_REPOSITORY && refreshCache === true) {
core.info( core.info(

View File

@ -22,32 +22,61 @@ export function isExactKeyMatch(key: string, cacheKey?: string): boolean {
); );
} }
export async function deleteCacheByKey(key: string, owner: string, repo: string) { export function logWarning(message: string): void {
const warningPrefix = "[warning]";
core.info(`${warningPrefix}${message}`);
}
export async function deleteCacheByKey(key: string, owner: string, repo: string) : Promise <number | void> {
const octokit = new Octokit(); const octokit = new Octokit();
let response; let response;
try { try {
response = await octokit.rest.actions.deleteActionsCacheByKey({ const gitRef = process.env[RefKey];
let cacheEntry = await octokit.rest.actions.getActionsCacheList({
owner: owner, owner: owner,
repo: repo, repo: repo,
key: key key: key,
ref: gitRef
}); });
if (response.status === 200) { const { data: {
core.info(`Succesfully deleted cache with key: ${response.data.actions_caches[0].key}`); total_count,
actions_caches
}
} = cacheEntry;
if (total_count !== 1 || total_count !== actions_caches.length) { // leave all find logic to the actual cache implementation. We just want to make sure we're returned a single element so we don't accidentally delete an entry that belongs to a different gitref.
if (total_count > 1) {
exports.logWarning(`More than one cache entry found for key ${key}`);
}
else if (total_count === 0 || actions_caches.length === 0) {
exports.logWarning(`No cache entries for key ${key} belong to gitref ${gitRef}.`);
}
// This situation is likely never actually going to come up.
// Istanbul is being dumb and I can't ignore this path.
else if (total_count !== actions_caches.length) {
exports.logWarning(`Reported cache entry matches for ${key} does not match length of 'actions_caches' array in API response.`);
}
core.info(`Skip trying to delete cache entry for key ${key}.`)
return;
}
let id = actions_caches[0].id;
response = await octokit.rest.actions.deleteActionsCacheById({
owner: owner,
repo: repo,
cache_id: id
});
if (response.status === 204) {
core.info(`Succesfully deleted cache with key: ${key}, id: ${id}`);
return 204;
} }
} catch (e) { } catch (e) {
if (e instanceof RequestError) { if (e instanceof RequestError) {
let err = e as RequestError; let err = e as RequestError;
let errData = err.response?.data as any | undefined; let errData = err.response?.data as any | undefined;
exports.logWarning(`${err.name} '${err.status}: ${errData?.message}' trying to delete cache with key: ${key}`); exports.logWarning(`Github API reported error: ${err.name} '${err.status}: ${errData?.message}'`);
} }
response = e; core.info(`Couldn't delete cache entry for key ${key}.`)
return;
} }
return response;
}
export function logWarning(message: string): void {
const warningPrefix = "[warning]";
core.info(`${warningPrefix}${message}`);
} }
// Cache token authorized for all events that are tied to a ref // Cache token authorized for all events that are tied to a ref

View File

@ -1,12 +1,16 @@
/* istanbul ignore file */
import { Inputs } from "../constants"; import { Inputs } from "../constants";
import { rest } from "msw"; import { rest } from "msw";
import { setupServer } from "msw/node"; import { setupServer } from "msw/node";
import nock from "nock"; import nock from "nock";
export const successCacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; export const successCacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
export const wrongRefCacheKey = "Linux-latest-node-bb828da54c148048dd17899ba9fda624811cfb43";
export const failureCacheKey = "Windows-node-bb828da54c148048dd17899ba9fda624811cfb43"; export const failureCacheKey = "Windows-node-bb828da54c148048dd17899ba9fda624811cfb43";
export const passThroughCacheKey = "macOS-node-bb828da54c148048dd17899ba9fda624811cfb43"; export const passThroughCacheKey = "macOS-node-bb828da54c148048dd17899ba9fda624811cfb43";
const successCacheId = 1337;
const failureCacheId = 69;
// See: https://github.com/actions/toolkit/blob/master/packages/core/src/core.ts#L67 // See: https://github.com/actions/toolkit/blob/master/packages/core/src/core.ts#L67
function getInputName(name: string): string { function getInputName(name: string): string {
@ -56,31 +60,80 @@ export function clearInputs(): void {
delete process.env[getInputName(Inputs.RefreshCache)]; delete process.env[getInputName(Inputs.RefreshCache)];
} }
/* istanbul ignore next */ export const mockServer = setupServer(
export const mockServer = setupServer(rest.delete('https://api.github.com/repos/owner/repo/actions/caches', (req, res, ctx) => { rest.delete('https://api.github.com/repos/owner/repo/actions/caches/', (req, res, ctx) => {
if (req.url?.searchParams?.get('key') === failureCacheKey) { return res(ctx.status(422),
ctx.json({
message: "Invalid request.\n\nMissing required query parameter key",
documentation_url: "https://docs.github.com/rest/actions/cache#delete-github-actions-caches-for-a-repository-using-a-cache-key",
})
)
}),
rest.delete('https://api.github.com/repos/owner/repo/actions/caches/:id', (req, res, ctx) => {
const { id } = req.params;
if (parseInt(id as string) === failureCacheId) {
return res(ctx.status(404), return res(ctx.status(404),
ctx.json({ ctx.json({
message: "Not Found", message: "Not Found",
documentation_url: "https://docs.github.com/rest/actions/cache#delete-github-actions-caches-for-a-repository-using-a-cache-key" documentation_url: "https://docs.github.com/rest/actions/cache#delete-a-github-actions-cache-for-a-repository-using-a-cache-id"
})); }));
} }
else if (req.url?.searchParams?.get('key') === successCacheKey) { return res(ctx.status(204));
}),
// This endpoint always returns 200/OK, what we're checking here is whether we can get a unique cache ID, to avoid deleting the wrong entry.
rest.get('https://api.github.com/repos/owner/repo/actions/caches', (req, res, ctx) => {
let key : string = req.url?.searchParams?.get('key') || '';
let ref : string = req.url?.searchParams?.get('ref') || '';
if (key === '' || ref === '') {
return res(ctx.status(200),
ctx.json({
total_count: 2,
actions_caches: [{
id: 15,
ref: "refs/heads/main",
key: failureCacheKey,
version: "73885106f58cc52a7df9ec4d4a5622a5614813162cb516c759a30af6bf56e6f0",
last_accessed_at: "2022-12-29T22:06:42.683333300Z",
created_at: "2022-12-29T22:06:42.683333300Z",
size_in_bytes: 6057793
},
{
id: 16,
ref: "refs/heads/another-feature-branch",
key: failureCacheKey,
version: "73885106f58cc52a7df9ec4d4a5622a5614813162cb516c759a30af6bf56e6f0",
last_accessed_at: "2022-12-29T22:06:42.683333300Z",
created_at: "2022-12-29T22:06:42.683333300Z",
size_in_bytes: 6057793
}]
})
);
}
// This is the behavior seen when search doesn't find anything, but it is seen both when no key matches, as well as when the key matches but the entry belongs to another (likely the base) branch.
else if (key === wrongRefCacheKey) {
return res(ctx.status(200),
ctx.json({
total_count: 0,
actions_caches: []
})
);
}
else if (key === successCacheKey || key === failureCacheKey) {
return res(ctx.status(200), return res(ctx.status(200),
ctx.json({ ctx.json({
total_count: 1, total_count: 1,
actions_caches: [{ actions_caches: [{
id: 15, id: (key === successCacheKey ? successCacheId : failureCacheId),
ref: "refs/heads/main", ref: ref,
key: successCacheKey, key: key,
version: "93a0f912fdb70083e929c1bf564bca2050be1c4e0932f7f9e78465ddcfbcc8f6", version: "93a0f912fdb70083e929c1bf564bca2050be1c4e0932f7f9e78465ddcfbcc8f6",
last_accessed_at: "2022-12-29T22:06:42.683333300Z", last_accessed_at: "2022-12-29T22:06:42.683333300Z",
created_at: "2022-12-29T22:06:42.683333300Z", created_at: "2022-12-29T22:06:42.683333300Z",
size_in_bytes: 6057793 size_in_bytes: 6057793
}] }]
})); })
);
} }
else if (req.url?.searchParams?.get('key') === passThroughCacheKey) {
return req.passthrough(); return req.passthrough();
} })
})); );

View File

@ -1,90 +0,0 @@
import { Inputs } from "../constants";
import { rest } from "msw";
import { setupServer } from "msw/node";
import nock from "nock";
export const successCacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
export const failureCacheKey = "Windows-node-bb828da54c148048dd17899ba9fda624811cfb43";
export const passThroughCacheKey = "macOS-node-bb828da54c148048dd17899ba9fda624811cfb43";
// See: https://github.com/actions/toolkit/blob/master/packages/core/src/core.ts#L67
function getInputName(name: string): string {
return `INPUT_${name.replace(/ /g, "_").toUpperCase()}`;
}
export function setInput(name: string, value: string): void {
process.env[getInputName(name)] = value;
}
interface CacheInput {
path: string;
key: string;
restoreKeys?: string[];
enableCrossOsArchive?: boolean;
failOnCacheMiss?: boolean;
lookupOnly?: boolean;
refreshCache?: boolean;
}
export function setInputs(input: CacheInput): void {
setInput(Inputs.Path, input.path);
setInput(Inputs.Key, input.key);
input.restoreKeys &&
setInput(Inputs.RestoreKeys, input.restoreKeys.join("\n"));
input.enableCrossOsArchive !== undefined &&
setInput(
Inputs.EnableCrossOsArchive,
input.enableCrossOsArchive.toString()
);
input.failOnCacheMiss !== undefined &&
setInput(Inputs.FailOnCacheMiss, input.failOnCacheMiss.toString());
input.lookupOnly !== undefined &&
setInput(Inputs.LookupOnly, input.lookupOnly.toString());
<<<<<<< HEAD
input.refreshCache !== undefined &&
setInput(Inputs.RefreshCache, input.refreshCache.toString());
=======
input.refreshCache && setInput(Inputs.RefreshCache, input.refreshCache.toString());
>>>>>>> 0111818 (Allow refreshing cache also with granular save.)
}
export function clearInputs(): void {
delete process.env[getInputName(Inputs.Path)];
delete process.env[getInputName(Inputs.Key)];
delete process.env[getInputName(Inputs.RestoreKeys)];
delete process.env[getInputName(Inputs.UploadChunkSize)];
delete process.env[getInputName(Inputs.EnableCrossOsArchive)];
delete process.env[getInputName(Inputs.FailOnCacheMiss)];
delete process.env[getInputName(Inputs.LookupOnly)];
delete process.env[getInputName(Inputs.RefreshCache)];
}
/* istanbul ignore next */
export const mockServer = setupServer(rest.delete('https://api.github.com/repos/owner/repo/actions/caches', (req, res, ctx) => {
if (req.url?.searchParams?.get('key') === failureCacheKey) {
return res(ctx.status(404),
ctx.json({
message: "Not Found",
documentation_url: "https://docs.github.com/rest/actions/cache#delete-github-actions-caches-for-a-repository-using-a-cache-key"
}));
}
else if (req.url?.searchParams?.get('key') === successCacheKey) {
return res(ctx.status(200),
ctx.json({
total_count: 1,
actions_caches: [{
id: 15,
ref: "refs/heads/main",
key: successCacheKey,
version: "93a0f912fdb70083e929c1bf564bca2050be1c4e0932f7f9e78465ddcfbcc8f6",
last_accessed_at: "2022-12-29T22:06:42.683333300Z",
created_at: "2022-12-29T22:06:42.683333300Z",
size_in_bytes: 6057793
}]
}));
}
else if (req.url?.searchParams?.get('key') === passThroughCacheKey) {
return req.passthrough();
}
}));