fix(repo): keep one fallow audit base cache per checkout instead of one per main commit (#4525)

This commit is contained in:
Miguel Ángel
2026-09-26 07:36:06 +00:00
committed by GitHub
parent 516320c28d
commit 81d8a03d2e
4 changed files with 164 additions and 2 deletions
+2 -1
View File
@@ -30,7 +30,8 @@ pre-commit:
# Unset git worktree env vars: fallow creates a temp worktree internally and
# GIT_DIR/GIT_INDEX_FILE (set by git in worktree hook context) break that.
# env -u is safe in non-worktree contexts (no-op when var is unset).
run: env -u GIT_DIR -u GIT_INDEX_FILE -u GIT_WORK_TREE bunx fallow audit --base origin/main --fail-on-issues
# The prune keeps one audit base cache per checkout; the commit gets the audit's own status.
run: env -u GIT_DIR -u GIT_INDEX_FILE -u GIT_WORK_TREE sh -c 'bunx fallow audit --base origin/main --fail-on-issues; status=$?; bun scripts/prune-fallow-audit-caches.mjs origin/main; exit $status'
largefiles:
# Reject large binaries committed straight into the git pack instead of
# LFS. The script reads the staged set itself (via `git diff --cached`)
+1 -1
View File
@@ -52,7 +52,7 @@
"player:perf": "bun run --filter @hyperframes/player perf",
"format:check": "oxfmt --check .",
"knip": "knip",
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-comment-citations.test.mjs scripts/comments-workflow.test.mjs scripts/comment-ratchet.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-media-use-svg-sanitize-generated.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/merge-queue-workflows.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-publication.test.mjs scripts/ci/resolve-workflow-pr.test.mjs scripts/check-catalog-source-pr.test.mjs scripts/generate-registry-items.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-comment-citations.test.mjs scripts/comments-workflow.test.mjs scripts/comment-ratchet.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-media-use-svg-sanitize-generated.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/prune-fallow-audit-caches.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/merge-queue-workflows.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-publication.test.mjs scripts/ci/resolve-workflow-pr.test.mjs scripts/check-catalog-source-pr.test.mjs scripts/generate-registry-items.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
"typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json",
"test:skills": "node --test 'skills/**/*.test.mjs' 'packages/cli/src/media-use/**/*.test.mjs'",
"generate:previews": "tsx scripts/generate-template-previews.ts",
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bun
// fallow keeps one base-snapshot worktree per checkout and base commit in the temp dir
// (`fallow-audit-base-cache-<xxh3 of the checkout path>-<base sha[:16]>`); 2.75 never removes
// the one for an old base. Run after `fallow audit`: keeps only the current base's.
import { spawnSync } from "node:child_process";
import { readdirSync, realpathSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const PREFIX = "fallow-audit-base-cache-";
/** The entries in `names` that are this checkout's caches for a base other than `keepSha`. */
export function staleAuditCaches(names, repoHash, keepSha) {
const own = `${PREFIX}${repoHash}-`;
const keep = `${own}${keepSha.slice(0, 16)}`;
return names.filter((name) => name.startsWith(own) && !name.includes(".") && name !== keep);
}
function git(root, args) {
return spawnSync("git", args, { cwd: root, encoding: "utf8" });
}
if (import.meta.main) {
const base = process.argv[2] ?? "origin/main";
const root = realpathSync(git(process.cwd(), ["rev-parse", "--show-toplevel"]).stdout.trim());
const keepSha = git(root, ["rev-parse", base]).stdout.trim();
if (!keepSha) {
console.error(`prune-fallow-audit-caches: cannot resolve ${base}; nothing removed`);
process.exit(0);
}
const repoHash = Bun.hash.xxHash3(root).toString(16).padStart(16, "0");
const dir = tmpdir();
const stale = staleAuditCaches(readdirSync(dir), repoHash, keepSha);
for (const name of stale) {
// A registered cache must leave git's worktree list too, or `git worktree add` refuses the path later.
git(root, ["worktree", "remove", "--force", join(dir, name)]);
rmSync(join(dir, name), { recursive: true, force: true });
}
}
+122
View File
@@ -0,0 +1,122 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import {
appendFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, it } from "node:test";
import { staleAuditCaches } from "./prune-fallow-audit-caches.mjs";
const SCRIPT = join(import.meta.dirname, "prune-fallow-audit-caches.mjs");
const FALLOW = join(import.meta.dirname, "../node_modules/.bin/fallow");
const CAN_AUDIT = spawnSync("bun", ["--version"]).status === 0 && existsSync(FALLOW);
// Run from inside a git hook, these would point the temp repo's git at the outer checkout.
const ENV = { ...process.env };
for (const name of ["GIT_DIR", "GIT_INDEX_FILE", "GIT_WORK_TREE"]) delete ENV[name];
function git(cwd, ...args) {
const result = spawnSync("git", args, { cwd, env: ENV, encoding: "utf8" });
assert.equal(result.status, 0, result.stderr);
return result.stdout.trim();
}
function commit(repo, message) {
git(
repo,
"-c",
"user.email=t@t",
"-c",
"user.name=t",
"-c",
"commit.gpgsign=false",
"commit",
"-q",
"--no-verify",
"-am",
message,
);
}
describe("staleAuditCaches", () => {
it("picks this checkout's caches for other bases, nothing else", () => {
const names = [
"fallow-audit-base-cache-aaaaaaaaaaaaaaaa-1111111111111111",
"fallow-audit-base-cache-aaaaaaaaaaaaaaaa-2222222222222222",
"fallow-audit-base-cache-aaaaaaaaaaaaaaaa-1111111111111111.lock",
"fallow-audit-base-cache-bbbbbbbbbbbbbbbb-1111111111111111",
"fallow-audit-base-4242-99",
"unrelated",
];
assert.deepEqual(staleAuditCaches(names, "aaaaaaaaaaaaaaaa", "2222222222222222ffff"), [
"fallow-audit-base-cache-aaaaaaaaaaaaaaaa-1111111111111111",
]);
});
});
describe(
"prune-fallow-audit-caches",
{ skip: !CAN_AUDIT && "bun or fallow is not installed" },
() => {
it("leaves one fallow cache for the checkout after audits against two bases", () => {
const work = mkdtempSync(join(tmpdir(), "hf-fallow-prune-"));
try {
const repo = join(work, "repo");
const temp = join(work, "tmp");
mkdirSync(join(repo, "src"), { recursive: true });
mkdirSync(temp);
writeFileSync(
join(repo, "package.json"),
'{"name":"x","type":"module","main":"src/index.js"}',
);
writeFileSync(join(repo, "src/index.js"), "export const a = 1;\n");
git(repo, "init", "-q");
git(repo, "add", "-A");
commit(repo, "a");
const oldBase = git(repo, "rev-parse", "HEAD");
appendFileSync(join(repo, "src/index.js"), "export const b = 2;\n");
commit(repo, "b");
const newBase = git(repo, "rev-parse", "HEAD");
appendFileSync(join(repo, "src/index.js"), "export const c = 3;\n");
const env = { ...ENV, TMPDIR: temp };
for (const base of [oldBase, newBase]) {
const audit = spawnSync(FALLOW, ["audit", "--base", base], {
cwd: repo,
env,
encoding: "utf8",
});
assert.equal(audit.status, 0, audit.stderr);
}
const caches = () =>
readdirSync(temp).filter((name) =>
/^fallow-audit-base-cache-[0-9a-f]+-[0-9a-f]+$/.test(name),
);
assert.equal(caches().length, 2, "fallow left one cache per base");
const foreign = join(
temp,
`fallow-audit-base-cache-0000000000000000-${oldBase.slice(0, 16)}`,
);
mkdirSync(foreign);
const run = spawnSync("bun", [SCRIPT, newBase], { cwd: repo, env, encoding: "utf8" });
assert.equal(run.status, 0, run.stderr);
const left = caches().filter((name) => !foreign.endsWith(name));
assert.equal(left.length, 1);
assert.ok(left[0].endsWith(`-${newBase.slice(0, 16)}`), `kept ${left[0]}`);
assert.ok(existsSync(foreign), "another checkout's cache is not touched");
assert.doesNotMatch(git(repo, "worktree", "list"), new RegExp(oldBase.slice(0, 16)));
} finally {
rmSync(work, { recursive: true, force: true });
}
});
},
);