From bb7c24c3dd35d8974612f2828367f297b54c0411 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Tue, 15 Sep 2026 11:01:41 -0400 Subject: [PATCH 1/2] fix: only strip a literal "./" path prefix The prefix-stripping regex in normalizePath had an unescaped dot, so `/^.\//` matched any single character followed by a slash rather than just "./". Any module under a single-character top-level directory was reported without it: `x/config` came back as `config`, and every consumer of the output then failed on a directory that does not exist, e.g. golangci-lint with "working-directory (config) was not a path" and setup-go with "The specified go version file at: config/go.mod does not exist". Multi-character top-level directories were unaffected, which is why this went unnoticed: "pkg/" has no slash in its second position. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/chilled-paths-keep-x.md | 10 ++++++++++ actions/changed-modules-go/dist/index.js | 2 +- .../src/__tests__/path-ops.test.ts | 20 +++++++++++++++++++ actions/changed-modules-go/src/path-ops.ts | 2 +- actions/changed-roots/dist/index.js | 2 +- .../src/__tests__/path-ops.test.ts | 20 +++++++++++++++++++ actions/changed-roots/src/path-ops.ts | 2 +- 7 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 .changeset/chilled-paths-keep-x.md diff --git a/.changeset/chilled-paths-keep-x.md b/.changeset/chilled-paths-keep-x.md new file mode 100644 index 000000000..0ffc632a4 --- /dev/null +++ b/.changeset/chilled-paths-keep-x.md @@ -0,0 +1,10 @@ +--- +"changed-modules-go": patch +"changed-roots": patch +--- + +fix: only strip a literal "./" prefix when normalizing paths + +The prefix-stripping regex had an unescaped dot, so it matched any single character followed +by a slash. A module under a single-character top-level directory, such as `x/config`, was +reported as `config`, and consumers received a module directory no directory holds. diff --git a/actions/changed-modules-go/dist/index.js b/actions/changed-modules-go/dist/index.js index 3093794e5..f02702357 100644 --- a/actions/changed-modules-go/dist/index.js +++ b/actions/changed-modules-go/dist/index.js @@ -50649,7 +50649,7 @@ var import_micromatch = __toESM(require_micromatch()); function normalizePath(p) { let s = p.replace(/\\/g, "/"); if (s === "." || s === "./") return "."; - s = s.replace(/^.\//, "").replace(/\/+$/, ""); + s = s.replace(/^\.\//, "").replace(/\/+$/, ""); return s; } function sortDeepestFirst(mods) { diff --git a/actions/changed-modules-go/src/__tests__/path-ops.test.ts b/actions/changed-modules-go/src/__tests__/path-ops.test.ts index 7d8b51e53..f9330eca0 100644 --- a/actions/changed-modules-go/src/__tests__/path-ops.test.ts +++ b/actions/changed-modules-go/src/__tests__/path-ops.test.ts @@ -110,6 +110,26 @@ describe("matchModule", () => { const result = matchModule("scripts/build.js", moduleDirectories); expect(result).toBe("scripts"); }); + + // A single-character top-level directory used to be stripped along with the "./" prefix, + // so "x/config/go.mod" matched a module named "config" that no directory holds. + test("should keep a single-character top-level directory", () => { + const moduleDirectories = ["x/config"]; + const result = matchModule("x/config/go.mod", moduleDirectories); + expect(result).toBe("x/config"); + }); + + test('should still strip a leading "./" prefix', () => { + const moduleDirectories = ["x/config"]; + const result = matchModule("./x/config/go.mod", moduleDirectories); + expect(result).toBe("x/config"); + }); + + test('should normalize a module directory given with a "./" prefix', () => { + const moduleDirectories = ["./x/config"]; + const result = matchModule("x/config/go.mod", moduleDirectories); + expect(result).toBe("x/config"); + }); }); }); diff --git a/actions/changed-modules-go/src/path-ops.ts b/actions/changed-modules-go/src/path-ops.ts index 95baf17e9..b28a78718 100644 --- a/actions/changed-modules-go/src/path-ops.ts +++ b/actions/changed-modules-go/src/path-ops.ts @@ -14,7 +14,7 @@ function normalizePath(p: string): string { if (s === "." || s === "./") return "."; // strip leading ./ and extra slashes - s = s.replace(/^.\//, "").replace(/\/+$/, ""); + s = s.replace(/^\.\//, "").replace(/\/+$/, ""); return s; } diff --git a/actions/changed-roots/dist/index.js b/actions/changed-roots/dist/index.js index e416e1d25..0f2a207c5 100644 --- a/actions/changed-roots/dist/index.js +++ b/actions/changed-roots/dist/index.js @@ -50654,7 +50654,7 @@ var import_micromatch = __toESM(require_micromatch()); function normalizePath(p) { let s = p.replace(/\\/g, "/"); if (s === "." || s === "./") return "."; - s = s.replace(/^.\//, "").replace(/\/+$/, ""); + s = s.replace(/^\.\//, "").replace(/\/+$/, ""); return s; } function sortDeepestFirst(mods) { diff --git a/actions/changed-roots/src/__tests__/path-ops.test.ts b/actions/changed-roots/src/__tests__/path-ops.test.ts index 8b1814fec..e924b68f3 100644 --- a/actions/changed-roots/src/__tests__/path-ops.test.ts +++ b/actions/changed-roots/src/__tests__/path-ops.test.ts @@ -110,6 +110,26 @@ describe("matchModule", () => { const result = matchModule("scripts/build.js", moduleDirectories); expect(result).toBe("scripts"); }); + + // A single-character top-level directory used to be stripped along with the "./" prefix, + // so "x/config/go.mod" matched a module named "config" that no directory holds. + test("should keep a single-character top-level directory", () => { + const moduleDirectories = ["x/config"]; + const result = matchModule("x/config/go.mod", moduleDirectories); + expect(result).toBe("x/config"); + }); + + test('should still strip a leading "./" prefix', () => { + const moduleDirectories = ["x/config"]; + const result = matchModule("./x/config/go.mod", moduleDirectories); + expect(result).toBe("x/config"); + }); + + test('should normalize a module directory given with a "./" prefix', () => { + const moduleDirectories = ["./x/config"]; + const result = matchModule("x/config/go.mod", moduleDirectories); + expect(result).toBe("x/config"); + }); }); }); diff --git a/actions/changed-roots/src/path-ops.ts b/actions/changed-roots/src/path-ops.ts index dca542336..28abd132a 100644 --- a/actions/changed-roots/src/path-ops.ts +++ b/actions/changed-roots/src/path-ops.ts @@ -14,7 +14,7 @@ function normalizePath(p: string): string { if (s === "." || s === "./") return "."; // strip leading ./ and extra slashes - s = s.replace(/^.\//, "").replace(/\/+$/, ""); + s = s.replace(/^\.\//, "").replace(/\/+$/, ""); return s; } From d276246e5007b34125adac805f1ae0b574676b63 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Tue, 15 Sep 2026 11:46:57 -0400 Subject: [PATCH 2/2] fix test bug --- actions/changed-modules-go/src/__tests__/path-ops.test.ts | 4 ++-- actions/changed-roots/src/__tests__/path-ops.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/actions/changed-modules-go/src/__tests__/path-ops.test.ts b/actions/changed-modules-go/src/__tests__/path-ops.test.ts index f9330eca0..f9b92c397 100644 --- a/actions/changed-modules-go/src/__tests__/path-ops.test.ts +++ b/actions/changed-modules-go/src/__tests__/path-ops.test.ts @@ -127,8 +127,8 @@ describe("matchModule", () => { test('should normalize a module directory given with a "./" prefix', () => { const moduleDirectories = ["./x/config"]; - const result = matchModule("x/config/go.mod", moduleDirectories); - expect(result).toBe("x/config"); + const result = matchModules(["x/config/go.mod"], moduleDirectories); + expect(result).toEqual([["x/config/go.mod", "x/config"]]); }); }); }); diff --git a/actions/changed-roots/src/__tests__/path-ops.test.ts b/actions/changed-roots/src/__tests__/path-ops.test.ts index e924b68f3..e9a81bb9f 100644 --- a/actions/changed-roots/src/__tests__/path-ops.test.ts +++ b/actions/changed-roots/src/__tests__/path-ops.test.ts @@ -127,8 +127,8 @@ describe("matchModule", () => { test('should normalize a module directory given with a "./" prefix', () => { const moduleDirectories = ["./x/config"]; - const result = matchModule("x/config/go.mod", moduleDirectories); - expect(result).toBe("x/config"); + const result = matchModules(["x/config/go.mod"], moduleDirectories); + expect(result).toEqual([["x/config/go.mod", "x/config"]]); }); }); });