diff --git a/.gitignore b/.gitignore index 57a04f27..462aa908 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ tmp/ # Browser walkthrough harness screenshots + run summary (CL-6072) scripts/e2e/browser/shots/ + +# Lint caches — local only; CI installs fresh and rebuilds them. +.eslintcache diff --git a/bun.lock b/bun.lock index 8ec49346..e20232fb 100644 --- a/bun.lock +++ b/bun.lock @@ -1965,6 +1965,7 @@ "@intx/workflow-host": "0.3.0", "arktype": "^2.2.0", "better-auth": "1.6.29", + "hono": "4.13.3", }, "catalog": { "@modelcontextprotocol/sdk": "^1.30.0", @@ -3513,7 +3514,19 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@corbits/memory-hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#9e6f213", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-9e6f213", "sha512-utnM4ZT2zmslcPXYWAAqxlDNLcpGsXFiTOtj8h7+OXnhCP0Eaw8yl25+yCTyHpvt3jcdeG4h5uFsSj7ou0BZCA=="], + "@corbits/artifacts-hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], + + "@corbits/bench-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], + + "@corbits/chat-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], + + "@corbits/context-menu/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], + + "@corbits/plugins-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], + + "@corbits/settings-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], + + "@corbits/tasks-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], @@ -3537,7 +3550,9 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], - "@workbench/hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#9e6f213", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-9e6f213", "sha512-utnM4ZT2zmslcPXYWAAqxlDNLcpGsXFiTOtj8h7+OXnhCP0Eaw8yl25+yCTyHpvt3jcdeG4h5uFsSj7ou0BZCA=="], + "@workbench/hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], + + "@workbench/web/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], diff --git a/package.json b/package.json index cee30a9c..fd8f297d 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "check": "bun run typecheck && bun run lint && bun run test && bun run check:structural", "check:all": "bun run check && bun run check:packages", "typecheck": "bun run scripts/run-all.ts typecheck", - "lint": "prettier --check . && eslint .", + "lint": "prettier --check --cache . && eslint --cache .", "format": "prettier --write .", "build": "bun run scripts/run-all.ts build", "test": "bun test scripts/run-all.test.ts && bun run scripts/run-all.ts test", @@ -108,6 +108,7 @@ "@intx/workflow": "0.3.0", "@intx/workflow-deploy": "0.3.0", "@intx/workflow-host": "0.3.0", - "better-auth": "1.6.29" + "better-auth": "1.6.29", + "hono": "4.13.3" } } diff --git a/scripts/affected.test.ts b/scripts/affected.test.ts new file mode 100644 index 00000000..d70acbc1 --- /dev/null +++ b/scripts/affected.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, test } from "bun:test"; + +import { + affectedPackages, + directlyChanged, + isGlobalChange, + ownerOf, + withDependents, + type PackageManifest, +} from "./affected.ts"; + +const manifests: PackageManifest[] = [ + { name: "@corbits/events", dir: "packages/events", workspaceDeps: [] }, + { + name: "@corbits/chat", + dir: "packages/chat", + workspaceDeps: ["@corbits/events"], + }, + { + name: "@workbench/hub", + dir: "apps/hub", + workspaceDeps: ["@corbits/chat"], + }, + { name: "@corbits/slug", dir: "packages/slug", workspaceDeps: [] }, + { name: "@intx/db", dir: "vendor/intx/db", workspaceDeps: [] }, +]; + +describe("isGlobalChange", () => { + test("a root manifest or shared tsconfig forces a full run", () => { + expect(isGlobalChange(["package.json"])).toBe(true); + expect(isGlobalChange(["tsconfig.base.json"])).toBe(true); + }); + + test("anything under scripts/ forces a full run, since the gate itself moved", () => { + expect(isGlobalChange(["scripts/run-all.ts"])).toBe(true); + }); + + test("a package-local manifest is not global", () => { + expect(isGlobalChange(["packages/chat/package.json"])).toBe(false); + }); + + test("ordinary source changes are not global", () => { + expect(isGlobalChange(["packages/chat/src/index.ts"])).toBe(false); + }); +}); + +describe("ownerOf", () => { + test("attributes a file to its package", () => { + expect(ownerOf("packages/chat/src/index.ts", manifests)).toBe( + "@corbits/chat", + ); + }); + + test("prefers the longest matching root so a nested workspace is not shadowed", () => { + expect(ownerOf("vendor/intx/db/src/schema.ts", manifests)).toBe("@intx/db"); + }); + + test("returns undefined for a file no package owns", () => { + expect(ownerOf("README.md", manifests)).toBeUndefined(); + }); +}); + +describe("directlyChanged", () => { + test("collects each touched package once", () => { + const changed = directlyChanged( + [ + "packages/chat/src/a.ts", + "packages/chat/src/b.ts", + "packages/slug/src/c.ts", + ], + manifests, + ); + expect([...changed].sort()).toEqual(["@corbits/chat", "@corbits/slug"]); + }); +}); + +describe("withDependents", () => { + test("pulls in transitive dependents, not just direct ones", () => { + const affected = withDependents(new Set(["@corbits/events"]), manifests); + expect([...affected].sort()).toEqual([ + "@corbits/chat", + "@corbits/events", + "@workbench/hub", + ]); + }); + + test("a leaf package affects only itself", () => { + expect([...withDependents(new Set(["@corbits/slug"]), manifests)]).toEqual([ + "@corbits/slug", + ]); + }); + + test("terminates on a dependency cycle", () => { + const cyclic: PackageManifest[] = [ + { name: "a", dir: "packages/a", workspaceDeps: ["b"] }, + { name: "b", dir: "packages/b", workspaceDeps: ["a"] }, + ]; + expect([...withDependents(new Set(["a"]), cyclic)].sort()).toEqual([ + "a", + "b", + ]); + }); +}); + +describe("affectedPackages", () => { + test("a global change returns 'all' rather than a filtered set", () => { + expect(affectedPackages(["bun.lock"], manifests)).toBe("all"); + }); + + test("a package change returns that package and everything above it", () => { + const affected = affectedPackages( + ["packages/events/src/parse.ts"], + manifests, + ); + expect(affected).not.toBe("all"); + expect([...(affected as Set)].sort()).toEqual([ + "@corbits/chat", + "@corbits/events", + "@workbench/hub", + ]); + }); + + test("a change owned by no package checks nothing", () => { + expect([ + ...(affectedPackages(["docs/GLOSSARY.md"], manifests) as Set), + ]).toEqual([]); + }); +}); diff --git a/scripts/affected.ts b/scripts/affected.ts new file mode 100644 index 00000000..127a3e0e --- /dev/null +++ b/scripts/affected.ts @@ -0,0 +1,141 @@ +// Resolves which workspace packages a change actually affects, so a local +// gate can check those instead of all 109. +// +// A package is affected when the change touches its own files, or when it +// depends -- at any depth -- on a package that was touched. Dependents matter +// as much as the package itself: editing an exported type in +// `@corbits/agent-events` breaks its importers, not the package that changed. +// +// Some paths defeat the whole idea. A root manifest, the shared tsconfig, or +// the runner itself can change the result of every job, and no dependency edge +// records that. Those force a full run rather than a wrong-but-fast one -- +// a filtered gate that misses a break is worse than a slow one. +import { Glob } from "bun"; + +const WORKSPACE_ROOTS = [ + "apps", + "packages", + "tools", + "workflows", + "vendor/intx", +] as const; + +/** Changes whose blast radius no dependency edge can express. */ +const GLOBAL_PATHS = [ + "package.json", + "bun.lock", + "tsconfig.base.json", + "tsconfig.json", + "eslint.config.ts", + "scripts/", + ".github/", +] as const; + +export type PackageManifest = { + readonly name: string; + readonly dir: string; + readonly workspaceDeps: readonly string[]; +}; + +export function isGlobalChange(changedFiles: readonly string[]): boolean { + return changedFiles.some((file) => + GLOBAL_PATHS.some((global) => + global.endsWith("/") ? file.startsWith(global) : file === global, + ), + ); +} + +/** + * The package that owns a file, as the longest workspace directory prefixing + * it. Longest wins so a nested workspace root (`vendor/intx/db`) is not + * shadowed by a shorter one that happens to prefix it. + */ +export function ownerOf( + file: string, + manifests: readonly PackageManifest[], +): string | undefined { + let owner: PackageManifest | undefined; + for (const manifest of manifests) { + if (!file.startsWith(`${manifest.dir}/`)) continue; + if (owner === undefined || manifest.dir.length > owner.dir.length) { + owner = manifest; + } + } + return owner?.name; +} + +export function directlyChanged( + changedFiles: readonly string[], + manifests: readonly PackageManifest[], +): Set { + const changed = new Set(); + for (const file of changedFiles) { + const owner = ownerOf(file, manifests); + if (owner !== undefined) changed.add(owner); + } + return changed; +} + +/** + * Every package that reaches one of `seeds` through workspace dependencies, + * plus the seeds. Walks the reverse graph to a fixed point, so a cycle + * terminates instead of recursing forever. + */ +export function withDependents( + seeds: ReadonlySet, + manifests: readonly PackageManifest[], +): Set { + const dependentsOf = new Map(); + for (const manifest of manifests) { + for (const dep of manifest.workspaceDeps) { + const existing = dependentsOf.get(dep); + if (existing === undefined) dependentsOf.set(dep, [manifest.name]); + else existing.push(manifest.name); + } + } + + const affected = new Set(seeds); + const queue = [...seeds]; + while (queue.length > 0) { + const next = queue.pop(); + if (next === undefined) continue; + for (const dependent of dependentsOf.get(next) ?? []) { + if (affected.has(dependent)) continue; + affected.add(dependent); + queue.push(dependent); + } + } + return affected; +} + +export function affectedPackages( + changedFiles: readonly string[], + manifests: readonly PackageManifest[], +): Set | "all" { + if (isGlobalChange(changedFiles)) return "all"; + return withDependents(directlyChanged(changedFiles, manifests), manifests); +} + +export async function readManifests(): Promise { + const manifests: PackageManifest[] = []; + for (const root of WORKSPACE_ROOTS) { + const glob = new Glob(`${root}/*/package.json`); + for await (const manifestPath of glob.scan(".")) { + const raw = (await Bun.file(manifestPath).json()) as { + name?: string; + dependencies?: Record; + devDependencies?: Record; + }; + const dir = manifestPath.slice(0, -"/package.json".length); + const deps = { ...raw.dependencies, ...raw.devDependencies }; + manifests.push({ + name: raw.name ?? dir, + dir, + workspaceDeps: Object.entries(deps) + .filter(([, range]) => range.startsWith("workspace:")) + .map(([dep]) => dep), + }); + } + } + return manifests; +} diff --git a/scripts/run-all.ts b/scripts/run-all.ts index d6bfbaab..455f25e9 100644 --- a/scripts/run-all.ts +++ b/scripts/run-all.ts @@ -79,6 +79,63 @@ async function runJob(job: Job, script: string): Promise { return code; } +const SINCE_ENV = "WORKBENCH_CHECK_SINCE"; + +/** + * The files this change touches, relative to `ref`: everything committed since + * the merge base, plus whatever is still uncommitted. Uncommitted work counts + * because the gate runs before the commit exists. + */ +function changedFilesSince(ref: string): string[] { + const base = + Bun.spawnSync(["git", "merge-base", "HEAD", ref]) + .stdout.toString() + .trim() || ref; + const committed = Bun.spawnSync([ + "git", + "diff", + "--name-only", + `${base}...HEAD`, + ]); + const working = Bun.spawnSync(["git", "status", "--porcelain"]); + return [ + ...committed.stdout.toString().split("\n"), + ...working.stdout + .toString() + .split("\n") + .map((line) => line.slice(3)), + ] + .map((file) => file.trim()) + .filter((file) => file.length > 0); +} + +/** + * Narrows the job list to packages this change can actually break. Opt-in: + * without the env var every package runs, so CI and a bare `bun run check` + * keep their existing all-packages meaning. + */ +async function narrowToAffected(jobs: readonly Job[]): Promise { + const ref = process.env[SINCE_ENV]; + if (ref === undefined || ref === "") return [...jobs]; + + const { affectedPackages, readManifests } = await import("./affected.ts"); + const affected = affectedPackages( + changedFilesSince(ref), + await readManifests(), + ); + if (affected === "all") { + console.error( + `${SINCE_ENV}=${ref}: change is repo-wide, running every package`, + ); + return [...jobs]; + } + const narrowed = jobs.filter((job) => affected.has(job.name)); + console.error( + `${SINCE_ENV}=${ref}: ${narrowed.length} of ${jobs.length} package(s) affected`, + ); + return narrowed; +} + const script = process.argv[2]; if (!script) { console.error("usage: bun run scripts/run-all.ts "); @@ -93,9 +150,14 @@ try { process.exit(1); } -const jobs = await discover(script); +const discovered = await discover(script); +const jobs = await narrowToAffected(discovered); if (jobs.length === 0) { - console.log(`${script}: no workspace packages define it yet`); + const reason = + discovered.length === 0 + ? "no workspace packages define it yet" + : "nothing affected by this change"; + console.log(`${script}: ${reason}`); process.exit(0); }