Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions actions/dependency-cooldown/dist/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9549,8 +9549,9 @@ async function resolvePublishDates(dependencies, http) {

// src/scan.ts
import { execFile } from "node:child_process";
import { readFileSync as readFileSync2 } from "node:fs";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { basename as basename3, dirname as dirname2, join } from "node:path";
import { promisify } from "node:util";

// node_modules/tinyglobby/dist/index.mjs
Expand Down Expand Up @@ -12110,8 +12111,45 @@ function collectDetailed(lockfilePath, content) {
// src/scan.ts
var run = promisify(execFile);
var IGNORED_DIRECTORY_NAMES = [".git", "node_modules", "target", ".dart_tool"];
var CLOUDFLARE_PAGES_BUN_LOCKB_MARKER = "# THIS IS JUST DUMMY FILE FOR HELPING CLOUDFLARE DETECT BUN PACKAGE MANAGER (https://community.cloudflare.com/t/bun-not-detected-as-tool-when-using-new-bun-lock-instead-of-bun-lockb/779835)";
var CLOUDFLARE_PAGES_BUN_LOCKB_MARKER_BYTES = Buffer.from(CLOUDFLARE_PAGES_BUN_LOCKB_MARKER);
function siblingBunLock(lockbPath) {
const directory = dirname2(lockbPath);
return directory === "." ? "bun.lock" : `${directory}/bun.lock`;
}
function isCloudflarePagesDummyBunLockb(content) {
return content.equals(CLOUDFLARE_PAGES_BUN_LOCKB_MARKER_BYTES);
}
function isDummyBunLockbOnDisk(repoDir, lockbPath) {
return isCloudflarePagesDummyBunLockb(readFileSync2(join(repoDir, lockbPath)));
}
function omitDummyBunLockbCoveredByTextLock(lockfiles, dummyLockbPaths) {
const present = new Set(lockfiles);
return lockfiles.filter((path) => {
if (basename3(path) !== "bun.lockb") {
return true;
}
if (!present.has(siblingBunLock(path))) {
return true;
}
return !dummyLockbPaths.has(path);
});
}
function dummyBunLockbPathsOnDisk(repoDir, lockfiles) {
const present = new Set(lockfiles);
const dummies = /* @__PURE__ */ new Set();
for (const path of lockfiles) {
if (basename3(path) !== "bun.lockb" || !present.has(siblingBunLock(path))) {
continue;
}
if (isDummyBunLockbOnDisk(repoDir, path)) {
dummies.add(path);
}
}
return dummies;
}
function discoverLockfiles(repoDir) {
return globSync(
const lockfiles = globSync(
LOCKFILE_FILENAMES.map((filename) => `**/${filename}`),
{
cwd: repoDir,
Expand All @@ -12122,6 +12160,7 @@ function discoverLockfiles(repoDir) {
onlyFiles: true
}
).sort();
return omitDummyBunLockbCoveredByTextLock(lockfiles, dummyBunLockbPathsOnDisk(repoDir, lockfiles));
}
async function scanWorkingTree(repoDir, lockfiles) {
const result = { dependencies: [], uncheckable: [] };
Expand All @@ -12136,12 +12175,40 @@ async function scanWorkingTree(repoDir, lockfiles) {
function isIgnoredPath(path) {
return path.split("/").some((segment) => IGNORED_DIRECTORY_NAMES.includes(segment));
}
async function isDummyBunLockbAtRef(repoDir, ref, lockbPath) {
const { stdout } = await run("git", ["cat-file", "blob", `${ref}:${lockbPath}`], {
cwd: repoDir,
encoding: "buffer",
maxBuffer: 128 * 1024 * 1024
});
if (!Buffer.isBuffer(stdout)) {
throw new Error(`git cat-file blob ${ref}:${lockbPath} did not return a buffer`);
}
return isCloudflarePagesDummyBunLockb(stdout);
}
async function dummyBunLockbPathsAtRef(repoDir, ref, lockfiles) {
const present = new Set(lockfiles);
const dummies = /* @__PURE__ */ new Set();
for (const path of lockfiles) {
if (basename3(path) !== "bun.lockb" || !present.has(siblingBunLock(path))) {
continue;
}
if (await isDummyBunLockbAtRef(repoDir, ref, path)) {
dummies.add(path);
}
}
return dummies;
}
async function listLockfilesAtRef(repoDir, ref) {
const { stdout } = await run("git", ["ls-tree", "-r", "--name-only", ref], {
cwd: repoDir,
maxBuffer: 64 * 1024 * 1024
});
return stdout.split("\n").filter((path) => path.length > 0).filter((path) => LOCKFILE_FILENAMES.includes(path.split("/").pop())).filter((path) => !isIgnoredPath(path)).sort();
const lockfiles = stdout.split("\n").filter((path) => path.length > 0).filter((path) => LOCKFILE_FILENAMES.includes(path.split("/").pop())).filter((path) => !isIgnoredPath(path)).sort();
return omitDummyBunLockbCoveredByTextLock(
lockfiles,
await dummyBunLockbPathsAtRef(repoDir, ref, lockfiles)
);
}
async function readFileAtRef(repoDir, ref, lockfile) {
try {
Expand Down
11 changes: 9 additions & 2 deletions docs/dependency-cooldown.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,15 @@ can be established. They are listed in the job summary under "could not be
age-checked" instead of being silently ignored — a dependency the tool skipped
without saying so would be a hole in the policy. Review those by hand.

Bun's binary `bun.lockb` cannot be inspected at all and fails the run. Commit
the text lockfile instead:
Bun's binary `bun.lockb` cannot be inspected. A repository that only has
`bun.lockb` fails the run. A sibling `bun.lockb` is ignored only when its
bytes are identical to the Cloudflare Pages dummy used by Quantus checkouts
(`# THIS IS JUST DUMMY FILE...`). Length is not enough: a different file of
the same size is rejected. Package changes are still taken from `bun.lock`;
the dummy cannot encode a graph. A real or unknown `bun.lockb` next to
`bun.lock` is rejected, because the two files can diverge (Bun 1.1 still
installs from the binary). Bun's own migration is to generate `bun.lock` and
then delete `bun.lockb`. Commit the text lockfile with:

```bash
bun install --save-text-lockfile
Expand Down
105 changes: 102 additions & 3 deletions packages/dependency-cooldown/src/scan.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { execFile } from "node:child_process";
import { readFileSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { basename, dirname, join } from "node:path";
import { promisify } from "node:util";

import { globSync } from "tinyglobby";
Expand All @@ -21,8 +22,67 @@ const run = promisify(execFile);
*/
export const IGNORED_DIRECTORY_NAMES = [".git", "node_modules", "target", ".dart_tool"];

/**
* Exact dummy bun.lockb committed by Quantus Cloudflare Pages checkouts (docs,
* explorer, website). Identity is this whole buffer, not its length: a
* different 189-byte file is a real lockfile and fails closed.
*/
export const CLOUDFLARE_PAGES_BUN_LOCKB_MARKER =
"# THIS IS JUST DUMMY FILE FOR HELPING CLOUDFLARE DETECT BUN PACKAGE MANAGER (https://community.cloudflare.com/t/bun-not-detected-as-tool-when-using-new-bun-lock-instead-of-bun-lockb/779835)";

const CLOUDFLARE_PAGES_BUN_LOCKB_MARKER_BYTES = Buffer.from(CLOUDFLARE_PAGES_BUN_LOCKB_MARKER);

function siblingBunLock(lockbPath: string): string {
const directory = dirname(lockbPath);
return directory === "." ? "bun.lock" : `${directory}/bun.lock`;
}

function isCloudflarePagesDummyBunLockb(content: Buffer): boolean {
return content.equals(CLOUDFLARE_PAGES_BUN_LOCKB_MARKER_BYTES);
}

function isDummyBunLockbOnDisk(repoDir: string, lockbPath: string): boolean {
return isCloudflarePagesDummyBunLockb(readFileSync(join(repoDir, lockbPath)));
}

/**
* bun.lockb is listed so a binary-only repository fails closed. A sibling
* bun.lock is the inspectable source of truth only when bun.lockb is the
* known Cloudflare Pages dummy; a real or unknown paired bun.lockb is kept
* so the parser rejects it instead of skipping an unreadable lockfile.
*/
function omitDummyBunLockbCoveredByTextLock(
lockfiles: string[],
dummyLockbPaths: ReadonlySet<string>,
): string[] {
const present = new Set(lockfiles);
return lockfiles.filter((path) => {
if (basename(path) !== "bun.lockb") {
return true;
}
if (!present.has(siblingBunLock(path))) {
return true;
}
return !dummyLockbPaths.has(path);
});
}

function dummyBunLockbPathsOnDisk(repoDir: string, lockfiles: string[]): Set<string> {
const present = new Set(lockfiles);
const dummies = new Set<string>();
for (const path of lockfiles) {
if (basename(path) !== "bun.lockb" || !present.has(siblingBunLock(path))) {
continue;
}
if (isDummyBunLockbOnDisk(repoDir, path)) {
dummies.add(path);
}
}
return dummies;
}

export function discoverLockfiles(repoDir: string): string[] {
return globSync(
const lockfiles = globSync(
LOCKFILE_FILENAMES.map((filename) => `**/${filename}`),
{
cwd: repoDir,
Expand All @@ -33,6 +93,7 @@ export function discoverLockfiles(repoDir: string): string[] {
onlyFiles: true,
},
).sort();
return omitDummyBunLockbCoveredByTextLock(lockfiles, dummyBunLockbPathsOnDisk(repoDir, lockfiles));
}

export interface ScanResult {
Expand All @@ -55,6 +116,40 @@ function isIgnoredPath(path: string): boolean {
return path.split("/").some((segment) => IGNORED_DIRECTORY_NAMES.includes(segment));
}

async function isDummyBunLockbAtRef(
repoDir: string,
ref: string,
lockbPath: string,
): Promise<boolean> {
const { stdout } = await run("git", ["cat-file", "blob", `${ref}:${lockbPath}`], {
cwd: repoDir,
encoding: "buffer",
maxBuffer: 128 * 1024 * 1024,
});
if (!Buffer.isBuffer(stdout)) {
throw new Error(`git cat-file blob ${ref}:${lockbPath} did not return a buffer`);
}
return isCloudflarePagesDummyBunLockb(stdout);
}

async function dummyBunLockbPathsAtRef(
repoDir: string,
ref: string,
lockfiles: string[],
): Promise<Set<string>> {
const present = new Set(lockfiles);
const dummies = new Set<string>();
for (const path of lockfiles) {
if (basename(path) !== "bun.lockb" || !present.has(siblingBunLock(path))) {
continue;
}
if (await isDummyBunLockbAtRef(repoDir, ref, path)) {
dummies.add(path);
}
}
return dummies;
}

/**
* Lists lockfiles as they existed at `ref`, so that moving or renaming a
* lockfile does not make its whole content look newly introduced.
Expand All @@ -64,12 +159,16 @@ export async function listLockfilesAtRef(repoDir: string, ref: string): Promise<
cwd: repoDir,
maxBuffer: 64 * 1024 * 1024,
});
return stdout
const lockfiles = stdout
.split("\n")
.filter((path) => path.length > 0)
.filter((path) => LOCKFILE_FILENAMES.includes(path.split("/").pop() as string))
.filter((path) => !isIgnoredPath(path))
.sort();
return omitDummyBunLockbCoveredByTextLock(
lockfiles,
await dummyBunLockbPathsAtRef(repoDir, ref, lockfiles),
);
}

async function readFileAtRef(
Expand Down
58 changes: 58 additions & 0 deletions packages/dependency-cooldown/test/end-to-end.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { BYPASS_REASON_MARKER } from "../src/bypass.js";
import type { HttpClient } from "../src/registries/http.js";
import { DEFAULT_BYPASS_LABEL, run } from "../src/run.js";
import { CLOUDFLARE_PAGES_BUN_LOCKB_MARKER } from "../src/scan.js";
import { readFixture } from "./helpers/fixtures.js";

const NOW = new Date("2026-08-27T12:00:00Z");
Expand Down Expand Up @@ -247,6 +248,63 @@ describe("check mode", () => {
expect(summary()).toContain("Checked 6 newly introduced dependency version(s)");
});

it("checks bun.lock when the Cloudflare Pages dummy bun.lockb sits beside it", async () => {
write("bun.lock", readFixture("bun/base/bun.lock"));
write("bun.lockb", CLOUDFLARE_PAGES_BUN_LOCKB_MARKER);
const baseSha = commit("base");
write("bun.lock", readFixture("bun/head/bun.lock"));
commit("bump bun");

const code = await run(["--mode=check", `--base-ref=${baseSha}`, `--repo-dir=${repoDir}`], {
http: httpStub({ "tslib@2.8.1": true }),
now: NOW,
});

expect(code).toBe(1);
expect(summary()).toContain("tslib");
expect(summary()).toContain("bun.lock");
});

it("refuses a PR that replaces the dummy bun.lockb with a real lockfile while bun.lock is unchanged", async () => {
write("bun.lock", readFixture("bun/base/bun.lock"));
write("bun.lockb", CLOUDFLARE_PAGES_BUN_LOCKB_MARKER);
const baseSha = commit("base");
write("bun.lockb", "#!\u0000binary\u0000");
commit("swap in a real bun.lockb");

await expect(
run(["--mode=check", `--base-ref=${baseSha}`, `--repo-dir=${repoDir}`], {
http: httpStub({}),
now: NOW,
}),
).rejects.toThrow(/bun\.lockb/);
});

it("refuses a divergent non-marker bun.lockb sitting beside bun.lock", async () => {
write("bun.lock", readFixture("bun/base/bun.lock"));
write("bun.lockb", "#!\u0000binary\u0000");
const baseSha = commit("base");

await expect(
run(["--mode=check", `--base-ref=${baseSha}`, `--repo-dir=${repoDir}`], {
http: httpStub({}),
now: NOW,
}),
).rejects.toThrow(/bun\.lockb/);
});

it("still refuses a repository that only has bun.lockb", async () => {
write("bun.lockb", "#!\u0000binary\u0000");
const baseSha = commit("base");

await expect(
run(["--mode=check", `--base-ref=${baseSha}`, `--repo-dir=${repoDir}`], {
http: httpStub({}),
now: NOW,
}),
).rejects.toThrow(/bun\.lockb/);
});

it("refuses to run in a repository with no lockfile", async () => {
write("README.md", "nothing to lock\n");
const baseSha = commit("base");
Expand Down
Loading
Loading