mirror of
https://github.com/actions/cache.git
synced 2026-06-10 06:48:52 +00:00
Address copilot review feedback
This commit is contained in:
parent
dabc4c2ca1
commit
c855662eeb
13
.github/workflows/path-validation-e2e.yml
vendored
13
.github/workflows/path-validation-e2e.yml
vendored
@ -24,10 +24,15 @@ permissions:
|
|||||||
# - warn: the malicious entry is extracted but a workflow warning is logged.
|
# - warn: the malicious entry is extracted but a workflow warning is logged.
|
||||||
# - error: the malicious entry is rejected (no extraction).
|
# - error: the malicious entry is rejected (no extraction).
|
||||||
#
|
#
|
||||||
# NOTE: The poisoned-cache phase requires a small Node.js helper script
|
# NOTE: The poisoned-cache phase relies on a small Node.js helper script
|
||||||
# (__tests__/e2e/generate-poisoned-archive.mjs) that the test workflow invokes.
|
# (__tests__/e2e/save-poisoned-cache.mjs) that the workflow invokes. Rather
|
||||||
# We build the archive locally and upload it via the action under a strict-paths
|
# than fabricating a tar archive by hand, the helper calls the toolkit's
|
||||||
# label so the cache key namespacing remains consistent.
|
# `@actions/cache.saveCache()` with the declared `path` AND one or more extra
|
||||||
|
# paths that escape it; the toolkit packs everything into a normal cache
|
||||||
|
# archive. The action's later restore step declares only the legitimate
|
||||||
|
# `path`, so the extra entries become "escape" entries that the client-side
|
||||||
|
# validation should reject (or warn about) per the configured strict-paths
|
||||||
|
# mode.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
good-cache:
|
good-cache:
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
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 { Events, RefKey } from "../src/constants";
|
import { Events, Inputs, RefKey } from "../src/constants";
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
import * as testUtils from "../src/utils/testUtils";
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
|
|
||||||
@ -267,11 +267,11 @@ test("isGhes returns true when the GITHUB_SERVER_URL environment variable is set
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getPathValidationInput", () => {
|
describe("getPathValidationInput", () => {
|
||||||
const inputEnv = "INPUT_STRICT-PATHS";
|
const inputEnv = `INPUT_${Inputs.StrictPaths.toUpperCase()}`;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
delete process.env[inputEnv];
|
delete process.env[inputEnv];
|
||||||
// Re-mock getInput so the each-test environment reads INPUT_STRICT-PATHS
|
// Re-mock getInput so the each-test environment reads the input env var
|
||||||
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
|
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
|
||||||
return jest.requireActual("@actions/core").getInput(name, options);
|
return jest.requireActual("@actions/core").getInput(name, options);
|
||||||
});
|
});
|
||||||
@ -297,24 +297,21 @@ describe("getPathValidationInput", () => {
|
|||||||
expect(actionUtils.getPathValidationInput()).toBe(expected);
|
expect(actionUtils.getPathValidationInput()).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("falls back to 'warn' for unrecognized values and logs a warning", () => {
|
test("falls back to 'warn' for unrecognized values and emits a workflow warning", () => {
|
||||||
process.env[inputEnv] = "strict";
|
process.env[inputEnv] = "strict";
|
||||||
// getPathValidationInput() calls the same module's logWarning() via a
|
// Suppress the real implementation so the warning does not pollute
|
||||||
// local function reference (TypeScript compiles intra-module calls as
|
// the Jest log, and assert it was emitted via core.warning so it
|
||||||
// direct references, not exports.X lookups), so a jest.spyOn on
|
// surfaces as a real `::warning::` workflow annotation.
|
||||||
// actionUtils.logWarning cannot intercept it. Spy on core.info — the
|
const warningSpy = jest
|
||||||
// only externally observable side effect — and suppress the real
|
.spyOn(core, "warning")
|
||||||
// implementation so the warning does not pollute the Jest log.
|
|
||||||
const infoSpy = jest
|
|
||||||
.spyOn(core, "info")
|
|
||||||
.mockImplementation(() => undefined);
|
.mockImplementation(() => undefined);
|
||||||
try {
|
try {
|
||||||
expect(actionUtils.getPathValidationInput()).toBe("warn");
|
expect(actionUtils.getPathValidationInput()).toBe("warn");
|
||||||
expect(infoSpy).toHaveBeenCalledWith(
|
expect(warningSpy).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("Unrecognized value for strict-paths")
|
expect.stringContaining("Unrecognized value for strict-paths")
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
infoSpy.mockRestore();
|
warningSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -546,12 +546,12 @@ test("restore falls back to 'warn' when strict-paths input is unrecognized", asy
|
|||||||
const restoreCacheMock = jest
|
const restoreCacheMock = jest
|
||||||
.spyOn(cache, "restoreCache")
|
.spyOn(cache, "restoreCache")
|
||||||
.mockResolvedValueOnce(key);
|
.mockResolvedValueOnce(key);
|
||||||
// getPathValidationInput()'s call to logWarning() is intra-module so a
|
// getPathValidationInput() emits the misconfiguration notice via
|
||||||
// spy on actionUtils.logWarning would not intercept it. Spy on core.info
|
// core.warning() so it surfaces as a real `::warning::` workflow
|
||||||
// (the underlying transport for logWarning) and suppress the real
|
// annotation. Suppress the real implementation to keep the Jest log
|
||||||
// implementation so the warning does not print into the Jest log.
|
// clean while asserting it was called.
|
||||||
const infoMock = jest
|
const warningMock = jest
|
||||||
.spyOn(core, "info")
|
.spyOn(core, "warning")
|
||||||
.mockImplementation(() => undefined);
|
.mockImplementation(() => undefined);
|
||||||
|
|
||||||
await restoreImpl(new StateProvider());
|
await restoreImpl(new StateProvider());
|
||||||
@ -563,7 +563,7 @@ test("restore falls back to 'warn' when strict-paths input is unrecognized", asy
|
|||||||
{ lookupOnly: false, pathValidation: "warn" },
|
{ lookupOnly: false, pathValidation: "warn" },
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
expect(infoMock).toHaveBeenCalledWith(
|
expect(warningMock).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("Unrecognized value for strict-paths")
|
expect.stringContaining("Unrecognized value for strict-paths")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -49,7 +49,7 @@ errors per the `fail-on-cache-invalid` input.
|
|||||||
| defaults strict-paths to `'warn'` and forwards it to `restoreCache` | Default option object contains `pathValidation: 'warn'` |
|
| defaults strict-paths to `'warn'` and forwards it to `restoreCache` | Default option object contains `pathValidation: 'warn'` |
|
||||||
| `test.each(['off', 'warn', 'error'])` forwards each value to `restoreCache` | All three valid values reach the toolkit unchanged |
|
| `test.each(['off', 'warn', 'error'])` forwards each value to `restoreCache` | All three valid values reach the toolkit unchanged |
|
||||||
| falls back to `'warn'` when strict-paths input is unrecognized | Unknown values are coerced to `'warn'` and a warning is logged |
|
| falls back to `'warn'` when strict-paths input is unrecognized | Unknown values are coerced to `'warn'` and a warning is logged |
|
||||||
| treats `CacheIntegrityError` as a cache miss by default | When the toolkit throws `CacheIntegrityError` and `fail-on-cache-invalid: false`, action sets `cache-hit=false` and continues |
|
| treats `CacheIntegrityError` as a cache miss by default | When the toolkit throws `CacheIntegrityError` and `fail-on-cache-invalid: false`, action logs the rejection and returns without setting the `cache-hit` output (intentionally unset to match regular cache-miss semantics — see issue #1466) |
|
||||||
| fails when `CacheIntegrityError` is raised and `fail-on-cache-invalid: true` | When `fail-on-cache-invalid: true`, `core.setFailed()` is called with a message containing `integrity validation` and the code |
|
| fails when `CacheIntegrityError` is raised and `fail-on-cache-invalid: true` | When `fail-on-cache-invalid: true`, `core.setFailed()` is called with a message containing `integrity validation` and the code |
|
||||||
| propagates non-integrity errors normally | Network/auth errors still surface via `core.setFailed()` rather than being mis-classified as integrity failures |
|
| propagates non-integrity errors normally | Network/auth errors still surface via `core.setFailed()` rather than being mis-classified as integrity failures |
|
||||||
| `PARSE_ERROR` integrity failure also treated as miss by default | Validation handles both `PATH_VIOLATION` and `PARSE_ERROR` codes identically |
|
| `PARSE_ERROR` integrity failure also treated as miss by default | Validation handles both `PATH_VIOLATION` and `PARSE_ERROR` codes identically |
|
||||||
|
|||||||
@ -64,11 +64,16 @@ export async function restoreImpl(
|
|||||||
if (err instanceof Error && err.name === "CacheIntegrityError") {
|
if (err instanceof Error && err.name === "CacheIntegrityError") {
|
||||||
const code = (err as Error & { code?: string }).code;
|
const code = (err as Error & { code?: string }).code;
|
||||||
if (failOnCacheInvalid) {
|
if (failOnCacheInvalid) {
|
||||||
throw new Error(
|
// Preserve the toolkit's original error via `Error.cause`.
|
||||||
|
// (Assigned after construction because this project's
|
||||||
|
// tsconfig targets ES6.)
|
||||||
|
const failure = new Error(
|
||||||
`Restored cache failed integrity validation (${
|
`Restored cache failed integrity validation (${
|
||||||
code ?? "unknown"
|
code ?? "unknown"
|
||||||
}): ${err.message}`
|
}): ${err.message}`
|
||||||
);
|
);
|
||||||
|
(failure as Error & { cause?: unknown }).cause = err;
|
||||||
|
throw failure;
|
||||||
}
|
}
|
||||||
// Treat as a cache miss. Intentionally do NOT set the
|
// Treat as a cache miss. Intentionally do NOT set the
|
||||||
// `cache-hit` output here, to preserve the same downstream
|
// `cache-hit` output here, to preserve the same downstream
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
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 { RefKey } from "../constants";
|
import { Inputs, RefKey } from "../constants";
|
||||||
|
|
||||||
export function isGhes(): boolean {
|
export function isGhes(): boolean {
|
||||||
const ghUrl = new URL(
|
const ghUrl = new URL(
|
||||||
@ -70,15 +70,19 @@ export function getInputAsBool(
|
|||||||
* Read the `strict-paths` input and coerce it to a value the `@actions/cache`
|
* Read the `strict-paths` input and coerce it to a value the `@actions/cache`
|
||||||
* toolkit understands. Unknown values default to `'warn'` (a best-effort
|
* toolkit understands. Unknown values default to `'warn'` (a best-effort
|
||||||
* recovery — we don't want a typo to silently disable client-side validation)
|
* recovery — we don't want a typo to silently disable client-side validation)
|
||||||
* and a warning is emitted so the user notices.
|
* and a workflow warning annotation is emitted so the user notices.
|
||||||
|
*
|
||||||
|
* Uses `core.warning()` directly (rather than this module's `logWarning()`
|
||||||
|
* helper, which routes through `core.info()`) so an input misconfiguration
|
||||||
|
* surfaces as a real `::warning::` annotation in the run summary.
|
||||||
*/
|
*/
|
||||||
export function getPathValidationInput(): "off" | "warn" | "error" {
|
export function getPathValidationInput(): "off" | "warn" | "error" {
|
||||||
const raw = core.getInput("strict-paths") || "warn";
|
const raw = core.getInput(Inputs.StrictPaths) || "warn";
|
||||||
const normalized = raw.toLowerCase();
|
const normalized = raw.toLowerCase();
|
||||||
if (normalized === "off" || normalized === "warn" || normalized === "error") {
|
if (normalized === "off" || normalized === "warn" || normalized === "error") {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
logWarning(
|
core.warning(
|
||||||
`Unrecognized value for strict-paths: "${raw}". Falling back to "warn". Valid values are: off, warn, error.`
|
`Unrecognized value for strict-paths: "${raw}". Falling back to "warn". Valid values are: off, warn, error.`
|
||||||
);
|
);
|
||||||
return "warn";
|
return "warn";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user