Skip to content

Commit c83a540

Browse files
committed
chore(repo): forbid tracked generated artifacts
1 parent 2aadf45 commit c83a540

7 files changed

Lines changed: 88 additions & 2 deletions

File tree

lefthook.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ pre-commit:
3838
# split paths containing spaces. It skips files under the limit, anything
3939
# routed through LFS, and registry/ assets. Tune via HF_MAX_NONLFS_KB.
4040
run: ./scripts/check-large-files.sh
41+
tracked-artifacts:
42+
# Keep ignored dependency trees and platform metadata out of commits.
43+
# `git ls-files` observes the staged index, so staged removals pass and
44+
# an accidental force-add fails before the commit is created.
45+
run: bun run check:tracked-artifacts
4146
filesize:
4247
# Scoped to packages/studio — the 600 LOC limit is a studio architecture
4348
# standard enforced as part of the App.tsx decomposition work. Player and

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,16 @@
2424
"changelog:weekly": "tsx scripts/changelog-weekly.ts",
2525
"sync-schemas": "tsx scripts/sync-schemas.ts",
2626
"sync-schemas:check": "tsx scripts/sync-schemas.ts --check",
27-
"lint": "oxlint . && tsx scripts/lint-skills.ts",
27+
"lint": "bun run check:tracked-artifacts && oxlint . && tsx scripts/lint-skills.ts",
2828
"lint:skills": "tsx scripts/lint-skills.ts",
2929
"lint:fix": "oxlint --fix .",
30+
"check:tracked-artifacts": "node scripts/check-tracked-artifacts.mjs",
3031
"format": "oxfmt .",
3132
"test": "bun run --filter '*' test",
3233
"player:perf": "bun run --filter @hyperframes/player perf",
3334
"format:check": "oxfmt --check .",
3435
"knip": "knip",
35-
"test:scripts": "node --import tsx --test scripts/validate-release-channel.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/verify-packed-manifests.test.mjs",
36+
"test:scripts": "node --import tsx --test scripts/check-tracked-artifacts.test.mjs scripts/validate-release-channel.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/verify-packed-manifests.test.mjs",
3637
"test:skills": "node --test 'skills/**/*.test.mjs'",
3738
"generate:previews": "tsx scripts/generate-template-previews.ts",
3839
"generate:catalog-previews": "tsx scripts/generate-catalog-previews.ts",

packages/producer/.DS_Store

-8 KB
Binary file not shown.

packages/producer/tests/.DS_Store

-6 KB
Binary file not shown.
-6 KB
Binary file not shown.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { spawnSync } from "node:child_process";
2+
import { resolve } from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
5+
const FORBIDDEN_BASENAMES = new Set([".DS_Store"]);
6+
7+
export function isForbiddenTrackedPath(filePath) {
8+
const normalized = filePath.replaceAll("\\", "/");
9+
const segments = normalized.split("/");
10+
return segments.includes("node_modules") || FORBIDDEN_BASENAMES.has(segments.at(-1));
11+
}
12+
13+
export function findForbiddenTrackedPaths(filePaths) {
14+
return filePaths.filter(isForbiddenTrackedPath).sort();
15+
}
16+
17+
export function readTrackedPaths(cwd = process.cwd()) {
18+
const result = spawnSync("git", ["ls-files", "-z"], {
19+
cwd,
20+
encoding: "utf8",
21+
maxBuffer: 16 * 1024 * 1024,
22+
});
23+
if (result.error) throw result.error;
24+
if (result.status !== 0) {
25+
throw new Error(result.stderr.trim() || `git ls-files exited with status ${result.status}`);
26+
}
27+
return result.stdout.split("\0").filter(Boolean);
28+
}
29+
30+
export function checkTrackedArtifacts(cwd = process.cwd()) {
31+
return findForbiddenTrackedPaths(readTrackedPaths(cwd));
32+
}
33+
34+
function main() {
35+
const forbidden = checkTrackedArtifacts();
36+
if (forbidden.length === 0) {
37+
console.log("Tracked artifact check passed.");
38+
return;
39+
}
40+
41+
console.error("Forbidden generated artifacts are tracked by Git:");
42+
for (const filePath of forbidden) console.error(`- ${filePath}`);
43+
console.error("Remove these paths from the index; .gitignore already excludes them.");
44+
process.exitCode = 1;
45+
}
46+
47+
const entryPath = process.argv[1] ? resolve(process.argv[1]) : null;
48+
if (entryPath === fileURLToPath(import.meta.url)) main();
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import assert from "node:assert/strict";
2+
import { describe, it } from "node:test";
3+
import { findForbiddenTrackedPaths, isForbiddenTrackedPath } from "./check-tracked-artifacts.mjs";
4+
5+
describe("tracked artifact check", () => {
6+
it("rejects node_modules at any directory depth", () => {
7+
assert.equal(isForbiddenTrackedPath("node_modules/pkg/index.js"), true);
8+
assert.equal(isForbiddenTrackedPath("packages/producer/node_modules/pkg"), true);
9+
assert.equal(isForbiddenTrackedPath("packages\\producer\\node_modules\\pkg"), true);
10+
});
11+
12+
it("rejects platform metadata by basename", () => {
13+
assert.equal(isForbiddenTrackedPath(".DS_Store"), true);
14+
assert.equal(isForbiddenTrackedPath("packages/producer/tests/.DS_Store"), true);
15+
});
16+
17+
it("does not reject similarly named source paths", () => {
18+
assert.equal(isForbiddenTrackedPath("docs/node_modules-policy.md"), false);
19+
assert.equal(isForbiddenTrackedPath("packages/producer/src/DS_Store.ts"), false);
20+
});
21+
22+
it("returns a deterministic sorted list", () => {
23+
assert.deepEqual(
24+
findForbiddenTrackedPaths([
25+
"packages/z/.DS_Store",
26+
"packages/producer/src/index.ts",
27+
"packages/a/node_modules/pkg",
28+
]),
29+
["packages/a/node_modules/pkg", "packages/z/.DS_Store"],
30+
);
31+
});
32+
});

0 commit comments

Comments
 (0)