From ec060e3256ddd87c90c49f4a1c82332e5e766030 Mon Sep 17 00:00:00 2001 From: Beast Date: Fri, 28 Aug 2026 11:47:42 +0800 Subject: [PATCH 1/2] fix: js parser --- actions/dependency-cooldown/dist/index.mjs | 14 +++- .../dependency-cooldown/src/lockfiles/bun.ts | 18 +++++ .../test/end-to-end.test.ts | 30 ++++++++ .../test/lockfiles.test.ts | 69 +++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) diff --git a/actions/dependency-cooldown/dist/index.mjs b/actions/dependency-cooldown/dist/index.mjs index 6af93f3..1515a1a 100644 --- a/actions/dependency-cooldown/dist/index.mjs +++ b/actions/dependency-cooldown/dist/index.mjs @@ -11235,8 +11235,16 @@ var NON_REGISTRY_PROTOCOLS = [ "git+", "github:", "http:", - "https:" + "https:", + "root:" ]; +var LOCAL_TARBALL_EXTENSIONS = [".tgz", ".tar.gz", ".tar"]; +function isLocalTarballSpecifier(specifier) { + if (specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/")) { + return true; + } + return LOCAL_TARBALL_EXTENSIONS.some((extension) => specifier.endsWith(extension)); +} function splitDescriptor(descriptor) { const separator = descriptor.indexOf("@", 1); if (separator <= 0) { @@ -11283,6 +11291,10 @@ var bunLockfile = { uncheckable.push({ name, version: null, reason: `resolved via ${protocol}` }); continue; } + if (isLocalTarballSpecifier(specifier)) { + uncheckable.push({ name, version: null, reason: "resolved via local tarball" }); + continue; + } refs.push({ registry: "npm", name, version: specifier }); } return { refs, uncheckable }; diff --git a/packages/dependency-cooldown/src/lockfiles/bun.ts b/packages/dependency-cooldown/src/lockfiles/bun.ts index 0c00435..614f7f9 100644 --- a/packages/dependency-cooldown/src/lockfiles/bun.ts +++ b/packages/dependency-cooldown/src/lockfiles/bun.ts @@ -7,6 +7,10 @@ import type { DependencyRef, LockfileFormat, ParsedLockfile } from "../types.js" * `"": ["@", registry, meta, integrity]`. Non-registry * dependencies use a descriptor such as `name@workspace:path` or * `name@git+https://...` instead of a semver version. + * + * Local tarballs are a separate resolution type: Bun writes `name@./file.tgz` + * with no `file:` prefix (folders use `file:`, remote tarball URLs use + * `http:`/`https:`). */ const NON_REGISTRY_PROTOCOLS = [ "workspace:", @@ -16,8 +20,18 @@ const NON_REGISTRY_PROTOCOLS = [ "github:", "http:", "https:", + "root:", ]; +const LOCAL_TARBALL_EXTENSIONS = [".tgz", ".tar.gz", ".tar"]; + +function isLocalTarballSpecifier(specifier: string): boolean { + if (specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/")) { + return true; + } + return LOCAL_TARBALL_EXTENSIONS.some((extension) => specifier.endsWith(extension)); +} + function splitDescriptor(descriptor: string): { name: string; specifier: string } { // The separator is the first @ after index 0: a scoped name starts with @, // and a git specifier may itself contain one (git+ssh://git@host/...). @@ -73,6 +87,10 @@ export const bunLockfile: LockfileFormat = { uncheckable.push({ name, version: null, reason: `resolved via ${protocol}` }); continue; } + if (isLocalTarballSpecifier(specifier)) { + uncheckable.push({ name, version: null, reason: "resolved via local tarball" }); + continue; + } refs.push({ registry: "npm", name, version: specifier }); } diff --git a/packages/dependency-cooldown/test/end-to-end.test.ts b/packages/dependency-cooldown/test/end-to-end.test.ts index 341c611..e14e59f 100644 --- a/packages/dependency-cooldown/test/end-to-end.test.ts +++ b/packages/dependency-cooldown/test/end-to-end.test.ts @@ -386,4 +386,34 @@ describe("audit mode", () => { expect(code).toBe(0); }); + + it("audits a bun.lock that pins a local tarball without querying the registry for it", async () => { + write( + "bun.lock", + `{ + "lockfileVersion": 1, + "packages": { + "left-pad": ["left-pad@1.3.0", "", {}, "sha512-bbbb"], + "human-readable-checksum": ["human-readable-checksum@./package/human-readable-checksum-0.3.0.tgz", {}] + } + }`, + ); + commit("base"); + + const inner = httpStub({}); + const http: HttpClient = { + async getJson(url) { + if (url === "https://registry.npmjs.org/human-readable-checksum") { + throw new Error(`GET ${url} failed with HTTP 404 Not Found`); + } + return inner.getJson(url); + }, + }; + + const code = await run(["--mode=audit", `--repo-dir=${repoDir}`], { http, now: NOW }); + + expect(code).toBe(0); + expect(summary()).toContain("human-readable-checksum"); + expect(summary()).toContain("could not be age-checked"); + }); }); diff --git a/packages/dependency-cooldown/test/lockfiles.test.ts b/packages/dependency-cooldown/test/lockfiles.test.ts index be97684..0c598cd 100644 --- a/packages/dependency-cooldown/test/lockfiles.test.ts +++ b/packages/dependency-cooldown/test/lockfiles.test.ts @@ -44,6 +44,32 @@ describe("npm package-lock.json", () => { ), ).toThrow(/lockfileVersion 1/); }); + + it("reports a local tarball as uncheckable rather than treating it as a registry version", () => { + const parsed = npmLockfile.parse( + JSON.stringify({ + lockfileVersion: 3, + packages: { + "": { name: "demo-app", version: "1.0.0" }, + "node_modules/human-readable-checksum": { + version: "0.3.0", + resolved: "file:package/human-readable-checksum-0.3.0.tgz", + }, + }, + }), + "package-lock.json", + ); + + expect(parsed.refs).toEqual([]); + expect(parsed.uncheckable).toEqual([ + { + name: "human-readable-checksum", + version: "0.3.0", + reason: + "not resolved from registry.npmjs.org (file:package/human-readable-checksum-0.3.0.tgz)", + }, + ]); + }); }); describe("bun.lock", () => { @@ -90,6 +116,49 @@ describe("bun.lock", () => { "workspace-lib", ]); }); + + it("reports a relative local tarball as uncheckable rather than treating it as a registry version", () => { + const parsed = bunLockfile.parse( + '{ "packages": { "human-readable-checksum": ["human-readable-checksum@./package/human-readable-checksum-0.3.0.tgz", {}] } }', + "bun.lock", + ); + + expect(parsed.refs).toEqual([]); + expect(parsed.uncheckable).toEqual([ + { + name: "human-readable-checksum", + version: null, + reason: "resolved via local tarball", + }, + ]); + }); + + it.each(["../vendor/pkg-1.0.0.tgz", "/opt/pkg-1.0.0.tgz", "package/pkg-1.0.0.tgz"])( + "reports local tarball specifier %s as uncheckable", + (specifier) => { + const parsed = bunLockfile.parse( + `{ "packages": { "pkg": ["pkg@${specifier}", {}] } }`, + "bun.lock", + ); + + expect(parsed.refs).toEqual([]); + expect(parsed.uncheckable).toEqual([ + { name: "pkg", version: null, reason: "resolved via local tarball" }, + ]); + }, + ); + + it("reports the lockfile root entry as uncheckable", () => { + const parsed = bunLockfile.parse( + '{ "packages": { "demo-app": ["demo-app@root:", { "bin": { "demo": "bin.js" } }] } }', + "bun.lock", + ); + + expect(parsed.refs).toEqual([]); + expect(parsed.uncheckable).toEqual([ + { name: "demo-app", version: null, reason: "resolved via root:" }, + ]); + }); }); describe("pubspec.lock", () => { From 6d676629548cd49427fcce66d17b250162bfe33a Mon Sep 17 00:00:00 2001 From: Beast Date: Fri, 28 Aug 2026 12:57:22 +0800 Subject: [PATCH 2/2] fix: detect valid semver as local tarball --- actions/dependency-cooldown/dist/index.mjs | 5 ++++- package-lock.json | 6 ++++++ .../dependency-cooldown/src/lockfiles/bun.ts | 13 ++++++++++-- .../test/lockfiles.test.ts | 20 ++++++++++++++++++- 4 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 package-lock.json diff --git a/actions/dependency-cooldown/dist/index.mjs b/actions/dependency-cooldown/dist/index.mjs index 1515a1a..148e0d4 100644 --- a/actions/dependency-cooldown/dist/index.mjs +++ b/actions/dependency-cooldown/dist/index.mjs @@ -11245,6 +11245,9 @@ function isLocalTarballSpecifier(specifier) { } return LOCAL_TARBALL_EXTENSIONS.some((extension) => specifier.endsWith(extension)); } +function isNpmRegistryTuple(entry) { + return typeof entry[1] === "string"; +} function splitDescriptor(descriptor) { const separator = descriptor.indexOf("@", 1); if (separator <= 0) { @@ -11291,7 +11294,7 @@ var bunLockfile = { uncheckable.push({ name, version: null, reason: `resolved via ${protocol}` }); continue; } - if (isLocalTarballSpecifier(specifier)) { + if (!isNpmRegistryTuple(entry) && isLocalTarballSpecifier(specifier)) { uncheckable.push({ name, version: null, reason: "resolved via local tarball" }); continue; } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9ef88e5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "shared-workflows", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/packages/dependency-cooldown/src/lockfiles/bun.ts b/packages/dependency-cooldown/src/lockfiles/bun.ts index 614f7f9..401599d 100644 --- a/packages/dependency-cooldown/src/lockfiles/bun.ts +++ b/packages/dependency-cooldown/src/lockfiles/bun.ts @@ -10,7 +10,11 @@ import type { DependencyRef, LockfileFormat, ParsedLockfile } from "../types.js" * * Local tarballs are a separate resolution type: Bun writes `name@./file.tgz` * with no `file:` prefix (folders use `file:`, remote tarball URLs use - * `http:`/`https:`). + * `http:`/`https:`). Distinguishing them from registry versions must use the + * tuple, not the specifier suffix: npm prereleases such as `1.2.3-release.tgz` + * are valid, and Bun still stores them as `[name@version, registry, meta, + * integrity]` with a string registry field. Local tarballs put the metadata + * object in that slot: `[name@tarball, meta]`. */ const NON_REGISTRY_PROTOCOLS = [ "workspace:", @@ -32,6 +36,11 @@ function isLocalTarballSpecifier(specifier: string): boolean { return LOCAL_TARBALL_EXTENSIONS.some((extension) => specifier.endsWith(extension)); } +/** npm resolutions store a registry URL (or `""` for the default) at index 1. */ +function isNpmRegistryTuple(entry: unknown[]): boolean { + return typeof entry[1] === "string"; +} + function splitDescriptor(descriptor: string): { name: string; specifier: string } { // The separator is the first @ after index 0: a scoped name starts with @, // and a git specifier may itself contain one (git+ssh://git@host/...). @@ -87,7 +96,7 @@ export const bunLockfile: LockfileFormat = { uncheckable.push({ name, version: null, reason: `resolved via ${protocol}` }); continue; } - if (isLocalTarballSpecifier(specifier)) { + if (!isNpmRegistryTuple(entry) && isLocalTarballSpecifier(specifier)) { uncheckable.push({ name, version: null, reason: "resolved via local tarball" }); continue; } diff --git a/packages/dependency-cooldown/test/lockfiles.test.ts b/packages/dependency-cooldown/test/lockfiles.test.ts index 0c598cd..95791fe 100644 --- a/packages/dependency-cooldown/test/lockfiles.test.ts +++ b/packages/dependency-cooldown/test/lockfiles.test.ts @@ -117,6 +117,19 @@ describe("bun.lock", () => { ]); }); + it.each(["1.2.3-release.tgz", "1.0.0-beta.tar.gz", "2.0.0-rc.tar"])( + "collects registry semver %s even though it ends with a tarball extension", + (version) => { + const parsed = bunLockfile.parse( + `{ "packages": { "pkg": ["pkg@${version}", "", {}, "sha512-abcd"] } }`, + "bun.lock", + ); + + expect(parsed.uncheckable).toEqual([]); + expect(parsed.refs).toEqual([{ registry: "npm", name: "pkg", version }]); + }, + ); + it("reports a relative local tarball as uncheckable rather than treating it as a registry version", () => { const parsed = bunLockfile.parse( '{ "packages": { "human-readable-checksum": ["human-readable-checksum@./package/human-readable-checksum-0.3.0.tgz", {}] } }', @@ -133,7 +146,12 @@ describe("bun.lock", () => { ]); }); - it.each(["../vendor/pkg-1.0.0.tgz", "/opt/pkg-1.0.0.tgz", "package/pkg-1.0.0.tgz"])( + it.each([ + "../vendor/pkg-1.0.0.tgz", + "/opt/pkg-1.0.0.tgz", + "package/pkg-1.0.0.tgz", + "./1.2.3-release.tgz", + ])( "reports local tarball specifier %s as uncheckable", (specifier) => { const parsed = bunLockfile.parse(