From a706b41f2f1e74c9f34c4a0cf2f1b62cea8bf91d Mon Sep 17 00:00:00 2001 From: jan-kubica Date: Wed, 28 Oct 2026 09:02:14 +0100 Subject: [PATCH 1/2] fix: accept local actions at their checkout root --- .changeset/checkout-root-actions.md | 5 ++++ .../src/toolchain-checkout-sources.test.ts | 25 +++++++++++++++++++ .../src/toolchain-checkout-sources.ts | 13 +++++++++- .../src/toolchain-checkout-syntax.ts | 9 +++++++ .../oxlint-config/src/toolchain-runtime.ts | 8 ++---- 5 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 .changeset/checkout-root-actions.md diff --git a/.changeset/checkout-root-actions.md b/.changeset/checkout-root-actions.md new file mode 100644 index 00000000..9d164350 --- /dev/null +++ b/.changeset/checkout-root-actions.md @@ -0,0 +1,5 @@ +--- +"@stll/oxlint-config": patch +--- + +Allow reviewed local actions at a checkout destination while preserving version-file containment. diff --git a/packages/oxlint-config/src/toolchain-checkout-sources.test.ts b/packages/oxlint-config/src/toolchain-checkout-sources.test.ts index 5fd4f024..b7730922 100644 --- a/packages/oxlint-config/src/toolchain-checkout-sources.test.ts +++ b/packages/oxlint-config/src/toolchain-checkout-sources.test.ts @@ -357,3 +357,28 @@ test("directory-only sparse patterns never prove a version file", () => { ).toBe(true); } }); + +test("an action at the checkout root is valid without treating its directory as a version file", () => { + const selector = "snapshot"; + const entry = { ...declaration, selector, destination: selector }; + const source = { ...checkout, with: { ...checkout.with, path: selector } }; + expect( + proveCheckoutSource({ + declaration: entry, + checkoutSha: sha, + selector, + workflow: workflow([source, { ...consumer, uses: "./snapshot" }]), + }), + ).toBe(true); + expect( + proveCheckoutSource({ + declaration: entry, + checkoutSha: sha, + selector, + workflow: workflow([ + source, + { ...consumer, uses: "actions/setup-node@" + sha }, + ]), + }), + ).toBe(false); +}); diff --git a/packages/oxlint-config/src/toolchain-checkout-sources.ts b/packages/oxlint-config/src/toolchain-checkout-sources.ts index ae8acc48..8888984e 100644 --- a/packages/oxlint-config/src/toolchain-checkout-sources.ts +++ b/packages/oxlint-config/src/toolchain-checkout-sources.ts @@ -2,6 +2,7 @@ import path from "node:path"; import { checkoutActionRef, + checkoutContains, literalSparseCheckoutPath, normalizeCheckoutPath, selfCheckoutRepository, @@ -219,7 +220,17 @@ export const proveCheckoutSource = ({ return false; const selected = normalizeCheckoutPath(selector); const prefix = normalizeCheckoutPath(declaration.destination); - if (prefix !== "." && !selected.startsWith(`${prefix}/`)) return false; + if (!checkoutContains({ destination: prefix, selector: selected })) + return false; + if (selected === prefix) { + const uses = consumer["uses"]; + if ( + typeof uses !== "string" || + !uses.startsWith("./") || + normalizeCheckoutPath(uses.slice(2)) !== selected + ) + return false; + } for (const step of steps.slice(checkoutIndex + 1, consumerIndex)) { if (!record(step) || checkoutActionRef(step["uses"]) === undefined) continue; diff --git a/packages/oxlint-config/src/toolchain-checkout-syntax.ts b/packages/oxlint-config/src/toolchain-checkout-syntax.ts index 290ab89b..81433c27 100644 --- a/packages/oxlint-config/src/toolchain-checkout-syntax.ts +++ b/packages/oxlint-config/src/toolchain-checkout-syntax.ts @@ -30,3 +30,12 @@ export const sparseCheckoutPaths = (value: string) => .split(/\r?\n/) .map((entry) => entry.trim().replace(/^\//, "")) .filter((entry) => entry !== ""); + +type CheckoutContainment = { destination: string; selector: string }; +export const checkoutContains = ({ + destination, + selector, +}: CheckoutContainment) => + destination === "." || + selector === destination || + selector.startsWith(`${destination}/`); diff --git a/packages/oxlint-config/src/toolchain-runtime.ts b/packages/oxlint-config/src/toolchain-runtime.ts index 81619c7e..0df7f0e6 100644 --- a/packages/oxlint-config/src/toolchain-runtime.ts +++ b/packages/oxlint-config/src/toolchain-runtime.ts @@ -16,6 +16,7 @@ import { } from "./toolchain-checkout-sources"; import { checkoutActionRef, + checkoutContains, literalSparseCheckoutPath, normalizeCheckoutPath, selfCheckoutRepository, @@ -1123,12 +1124,7 @@ export const checkRuntimeFile = ({ if (selector === undefined) return checkoutBindings; let deepest: CheckoutBinding | undefined; for (const entry of checkoutBindings) { - if ( - entry.path !== "." && - selector !== entry.path && - !selector.startsWith(`${entry.path}/`) - ) - continue; + if (!checkoutContains({ destination: entry.path, selector })) continue; if ( deepest === undefined || (entry.path !== "." && From ef7124eb980e356c9754bae5a11c58858e80ae2e Mon Sep 17 00:00:00 2001 From: jan-kubica Date: Wed, 28 Oct 2026 09:17:36 +0100 Subject: [PATCH 2/2] fix: recognize local action sparse directory coverage --- .changeset/checkout-root-actions.md | 2 +- .../src/toolchain-checkout-sources.test.ts | 16 ++++++++++++++++ .../src/toolchain-checkout-sources.ts | 16 +++++++--------- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.changeset/checkout-root-actions.md b/.changeset/checkout-root-actions.md index 9d164350..7733cc31 100644 --- a/.changeset/checkout-root-actions.md +++ b/.changeset/checkout-root-actions.md @@ -2,4 +2,4 @@ "@stll/oxlint-config": patch --- -Allow reviewed local actions at a checkout destination while preserving version-file containment. +Allow reviewed local actions at a checkout destination and in directory-only sparse selections while preserving version-file containment. diff --git a/packages/oxlint-config/src/toolchain-checkout-sources.test.ts b/packages/oxlint-config/src/toolchain-checkout-sources.test.ts index b7730922..313745c7 100644 --- a/packages/oxlint-config/src/toolchain-checkout-sources.test.ts +++ b/packages/oxlint-config/src/toolchain-checkout-sources.test.ts @@ -382,3 +382,19 @@ test("an action at the checkout root is valid without treating its directory as }), ).toBe(false); }); + +test("directory-only sparse equality covers a local action directory", () => { + expect( + prove([ + { + ...checkout, + with: { + ...checkout.with, + "sparse-checkout": `${declaration.selector}/`, + "sparse-checkout-cone-mode": false, + }, + }, + consumer, + ]), + ).toBe(true); +}); diff --git a/packages/oxlint-config/src/toolchain-checkout-sources.ts b/packages/oxlint-config/src/toolchain-checkout-sources.ts index 8888984e..fb1119e1 100644 --- a/packages/oxlint-config/src/toolchain-checkout-sources.ts +++ b/packages/oxlint-config/src/toolchain-checkout-sources.ts @@ -222,15 +222,12 @@ export const proveCheckoutSource = ({ const prefix = normalizeCheckoutPath(declaration.destination); if (!checkoutContains({ destination: prefix, selector: selected })) return false; - if (selected === prefix) { - const uses = consumer["uses"]; - if ( - typeof uses !== "string" || - !uses.startsWith("./") || - normalizeCheckoutPath(uses.slice(2)) !== selected - ) - return false; - } + const uses = consumer["uses"]; + const localAction = + typeof uses === "string" && + uses.startsWith("./") && + normalizeCheckoutPath(uses.slice(2)) === selected; + if (selected === prefix && !localAction) return false; for (const step of steps.slice(checkoutIndex + 1, consumerIndex)) { if (!record(step) || checkoutActionRef(step["uses"]) === undefined) continue; @@ -278,6 +275,7 @@ export const proveCheckoutSource = ({ !patterns.some( (value) => relative === value || + (localAction && value === `${relative}/`) || relative.startsWith(value.endsWith("/") ? value : `${value}/`), ) )