diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index 007eac30c7..6388c912d2 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -8,6 +8,12 @@ permissions: env: HYPERFRAMES_NO_TELEMETRY: "1" +# Graphite can update a branch and its PR base separately for the same head +# SHA. Keep only the newest expensive regression run for each PR/ref. +concurrency: + group: regression-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + on: pull_request: push: @@ -111,7 +117,10 @@ jobs: load: true tags: hyperframes-producer:test cache-from: type=gha,scope=regression-test-image - cache-to: type=gha,mode=max,scope=regression-test-image + # PR matrices can fan out across many stacked branches at once. Let + # them consume the shared cache, but keep a single writer on main so + # concurrent exports cannot exhaust the Actions cache service. + cache-to: ${{ github.event_name == 'push' && 'type=gha,mode=max,scope=regression-test-image' || '' }} - name: "Run regression shard: ${{ matrix.shard }}" run: | diff --git a/lefthook.yml b/lefthook.yml index ac248273b5..3477e0df44 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -38,6 +38,11 @@ pre-commit: # split paths containing spaces. It skips files under the limit, anything # routed through LFS, and registry/ assets. Tune via HF_MAX_NONLFS_KB. run: ./scripts/check-large-files.sh + tracked-artifacts: + # Keep ignored dependency trees and platform metadata out of commits. + # `git ls-files` observes the staged index, so staged removals pass and + # an accidental force-add fails before the commit is created. + run: bun run check:tracked-artifacts filesize: # Scoped to packages/studio — the 600 LOC limit is a studio architecture # standard enforced as part of the App.tsx decomposition work. Player and diff --git a/package.json b/package.json index 33b4b16ce2..5535bb035e 100644 --- a/package.json +++ b/package.json @@ -24,15 +24,16 @@ "changelog:weekly": "tsx scripts/changelog-weekly.ts", "sync-schemas": "tsx scripts/sync-schemas.ts", "sync-schemas:check": "tsx scripts/sync-schemas.ts --check", - "lint": "oxlint . && tsx scripts/lint-skills.ts", + "lint": "bun run check:tracked-artifacts && oxlint . && tsx scripts/lint-skills.ts", "lint:skills": "tsx scripts/lint-skills.ts", "lint:fix": "oxlint --fix .", + "check:tracked-artifacts": "node scripts/check-tracked-artifacts.mjs", "format": "oxfmt .", "test": "bun run --filter '*' test", "player:perf": "bun run --filter @hyperframes/player perf", "format:check": "oxfmt --check .", "knip": "knip", - "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", + "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", "test:skills": "node --test 'skills/**/*.test.mjs'", "generate:previews": "tsx scripts/generate-template-previews.ts", "generate:catalog-previews": "tsx scripts/generate-catalog-previews.ts", diff --git a/packages/producer/.DS_Store b/packages/producer/.DS_Store deleted file mode 100644 index 277ac7d4c4..0000000000 Binary files a/packages/producer/.DS_Store and /dev/null differ diff --git a/packages/producer/tests/.DS_Store b/packages/producer/tests/.DS_Store deleted file mode 100644 index cb1dcac253..0000000000 Binary files a/packages/producer/tests/.DS_Store and /dev/null differ diff --git a/packages/producer/tests/style-1-prod/.DS_Store b/packages/producer/tests/style-1-prod/.DS_Store deleted file mode 100644 index 9a874b5768..0000000000 Binary files a/packages/producer/tests/style-1-prod/.DS_Store and /dev/null differ diff --git a/scripts/check-tracked-artifacts.mjs b/scripts/check-tracked-artifacts.mjs new file mode 100644 index 0000000000..eba42909b4 --- /dev/null +++ b/scripts/check-tracked-artifacts.mjs @@ -0,0 +1,48 @@ +import { spawnSync } from "node:child_process"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const FORBIDDEN_BASENAMES = new Set([".DS_Store"]); + +export function isForbiddenTrackedPath(filePath) { + const normalized = filePath.replaceAll("\\", "/"); + const segments = normalized.split("/"); + return segments.includes("node_modules") || FORBIDDEN_BASENAMES.has(segments.at(-1)); +} + +export function findForbiddenTrackedPaths(filePaths) { + return filePaths.filter(isForbiddenTrackedPath).sort(); +} + +export function readTrackedPaths(cwd = process.cwd()) { + const result = spawnSync("git", ["ls-files", "-z"], { + cwd, + encoding: "utf8", + maxBuffer: 16 * 1024 * 1024, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error(result.stderr.trim() || `git ls-files exited with status ${result.status}`); + } + return result.stdout.split("\0").filter(Boolean); +} + +export function checkTrackedArtifacts(cwd = process.cwd()) { + return findForbiddenTrackedPaths(readTrackedPaths(cwd)); +} + +function main() { + const forbidden = checkTrackedArtifacts(); + if (forbidden.length === 0) { + console.log("Tracked artifact check passed."); + return; + } + + console.error("Forbidden generated artifacts are tracked by Git:"); + for (const filePath of forbidden) console.error(`- ${filePath}`); + console.error("Remove these paths from the index; .gitignore already excludes them."); + process.exitCode = 1; +} + +const entryPath = process.argv[1] ? resolve(process.argv[1]) : null; +if (entryPath === fileURLToPath(import.meta.url)) main(); diff --git a/scripts/check-tracked-artifacts.test.mjs b/scripts/check-tracked-artifacts.test.mjs new file mode 100644 index 0000000000..4b9543c242 --- /dev/null +++ b/scripts/check-tracked-artifacts.test.mjs @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { findForbiddenTrackedPaths, isForbiddenTrackedPath } from "./check-tracked-artifacts.mjs"; + +describe("tracked artifact check", () => { + it("rejects node_modules at any directory depth", () => { + assert.equal(isForbiddenTrackedPath("node_modules/pkg/index.js"), true); + assert.equal(isForbiddenTrackedPath("packages/producer/node_modules/pkg"), true); + assert.equal(isForbiddenTrackedPath("packages\\producer\\node_modules\\pkg"), true); + }); + + it("rejects platform metadata by basename", () => { + assert.equal(isForbiddenTrackedPath(".DS_Store"), true); + assert.equal(isForbiddenTrackedPath("packages/producer/tests/.DS_Store"), true); + }); + + it("does not reject similarly named source paths", () => { + assert.equal(isForbiddenTrackedPath("docs/node_modules-policy.md"), false); + assert.equal(isForbiddenTrackedPath("packages/producer/src/DS_Store.ts"), false); + }); + + it("returns a deterministic sorted list", () => { + assert.deepEqual( + findForbiddenTrackedPaths([ + "packages/z/.DS_Store", + "packages/producer/src/index.ts", + "packages/a/node_modules/pkg", + ]), + ["packages/a/node_modules/pkg", "packages/z/.DS_Store"], + ); + }); +});