diff --git a/src/signals/path-matchers.ts b/src/signals/path-matchers.ts index fd81f38a94..4f354365b3 100644 --- a/src/signals/path-matchers.ts +++ b/src/signals/path-matchers.ts @@ -29,6 +29,7 @@ const LOCKFILE_NAMES: ReadonlySet = new Set([ "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml", + "bun.lock", "bun.lockb", "cargo.lock", "poetry.lock", @@ -39,12 +40,16 @@ const LOCKFILE_NAMES: ReadonlySet = new Set([ "uv.lock", "packages.lock.json", "flake.lock", + "deno.lock", + "pubspec.lock", + "podfile.lock", ]); const DEPENDENCY_MANIFEST_NAMES: ReadonlySet = new Set([ "package.json", "cargo.toml", "go.mod", + "go.work", "requirements.txt", "pyproject.toml", "pipfile", @@ -53,6 +58,11 @@ const DEPENDENCY_MANIFEST_NAMES: ReadonlySet = new Set([ "build.gradle", "build.gradle.kts", "pom.xml", + "deno.json", + "deno.jsonc", + "pubspec.yaml", + "mix.exs", + "uv.toml", ]); const DOCS_EXTENSIONS: ReadonlySet = new Set(["md", "mdx", "markdown", "rst", "adoc", "asciidoc"]); @@ -61,10 +71,22 @@ const DOCS_EXTENSIONS: ReadonlySet = new Set(["md", "mdx", "markdown", " const CONFIG_FILE_NAMES: ReadonlySet = new Set([ "dockerfile", "makefile", + "caddyfile", + "justfile", + "procfile", + "codeowners", ".editorconfig", ".nvmrc", ".npmrc", ".browserslistrc", + ".tool-versions", + ".pre-commit-config.yaml", + ".gitleaks.toml", + ".gittensory.yml", + ".gittensory.yml.example", + ".gitlab-ci.yml", + "azure-pipelines.yml", + "jenkinsfile", // Monorepo / task-runner config (Turborepo, Nx, Lerna). "turbo.json", "nx.json", @@ -72,6 +94,18 @@ const CONFIG_FILE_NAMES: ReadonlySet = new Set([ // Linter / formatter config that does not follow the `.eslintrc` / `*.config.*` shapes (Biome). "biome.json", "biome.jsonc", + // Deploy / automation / quality config. + "codecov.yml", + "renovate.json", + "railway.json", + "vercel.json", + "netlify.toml", + "mise.toml", + "cliff.toml", + "taskfile.yml", + "taskfile.yaml", + "docker-compose.yml", + "docker-compose.yaml", // VCS and build ignore/attribute config (siblings to the existing Dockerfile entry). ".gitignore", ".gitattributes", @@ -90,6 +124,8 @@ const CONFIG_FILE_PREFIXES: readonly string[] = [ "postcss.config", "tailwind.config", "next.config", + "playwright.config", + "eslint.config", ".env", ".eslint", ".prettier", @@ -99,6 +135,16 @@ const CONFIG_FILE_PREFIXES: readonly string[] = [ "wrangler.", ]; +/** CI workflow and automation entrypoints under conventional directories. */ +function isCiConfigPath(path: string): boolean { + const norm = normalize(path); + return ( + /(^|\/)\.github\/workflows\/[^/]+\.(ya?ml)$/.test(norm) || + /(^|\/)\.github\/dependabot\.ya?ml$/.test(norm) || + /(^|\/)\.circleci\/config\.ya?ml$/.test(norm) + ); +} + /** Machine-generated output (codegen, protobuf, source maps, typegen). */ export function isGeneratedFile(path: string): boolean { const norm = normalize(path); @@ -145,6 +191,7 @@ export function isDependencyManifestFile(path: string): boolean { * lower-effort than genuine source changes, so slop signals can weight them differently (#561). */ export function isConfigFile(path: string): boolean { + if (isCiConfigPath(path)) return true; const base = basename(path); if (CONFIG_FILE_NAMES.has(base)) return true; if (CONFIG_FILE_PREFIXES.some((prefix) => base.startsWith(prefix))) return true; diff --git a/test/unit/path-matchers.test.ts b/test/unit/path-matchers.test.ts index f1ca4a7b56..b10dc01c65 100644 --- a/test/unit/path-matchers.test.ts +++ b/test/unit/path-matchers.test.ts @@ -63,6 +63,10 @@ describe("isLockfile", () => { "go.sum", "uv.lock", "poetry.lock", + "deno.lock", + "bun.lock", + "pubspec.lock", + "ios/Podfile.lock", ]) { expect(isLockfile(path)).toBe(true); } @@ -115,7 +119,20 @@ describe("defensive input handling", () => { describe("isDependencyManifestFile", () => { it("matches dependency manifests", () => { - for (const path of ["package.json", "Cargo.toml", "go.mod", "requirements.txt", "pyproject.toml", "build.gradle.kts"]) { + for (const path of [ + "package.json", + "Cargo.toml", + "go.mod", + "go.work", + "requirements.txt", + "pyproject.toml", + "build.gradle.kts", + "deno.json", + "deno.jsonc", + "pubspec.yaml", + "mix.exs", + "uv.toml", + ]) { expect(isDependencyManifestFile(path)).toBe(true); } }); @@ -148,7 +165,7 @@ describe("isConfigFile", () => { } }); - it("matches monorepo, linter, and VCS/build config by exact basename", () => { + it("matches monorepo, linter, VCS/build, deploy, and toolchain config by exact basename", () => { for (const path of [ "turbo.json", "nx.json", @@ -158,11 +175,49 @@ describe("isConfigFile", () => { "packages/app/.gitignore", ".gitattributes", "services/api/.dockerignore", + ".pre-commit-config.yaml", + ".gitleaks.toml", + ".gittensory.yml", + ".gittensory.yml.example", + "codecov.yml", + "renovate.json", + "railway.json", + "vercel.json", + "netlify.toml", + "mise.toml", + ".tool-versions", + "Taskfile.yml", + "Taskfile.yaml", + "justfile", + "Caddyfile", + "docker-compose.yml", + "Procfile", + "CODEOWNERS", + ".gitlab-ci.yml", + "azure-pipelines.yml", + "Jenkinsfile", + ]) { + expect(isConfigFile(path)).toBe(true); + } + }); + + it("matches GitHub Actions and other CI workflow entrypoints", () => { + for (const path of [ + ".github/workflows/ci.yml", + ".github/workflows/release.yaml", + ".github/dependabot.yml", + ".circleci/config.yml", ]) { expect(isConfigFile(path)).toBe(true); } }); + it("matches test-runner and lint config by known filename prefix", () => { + for (const path of ["tsconfig.build.json", "vitest.config.ts", ".env.local", ".eslintrc.json", ".prettierrc.js", "playwright.config.ts", "eslint.config.js"]) { + expect(isConfigFile(path)).toBe(true); + } + }); + it("matches Cloudflare Workers deploy config by the wrangler prefix", () => { for (const path of ["wrangler.toml", "wrangler.jsonc", "apps/ui/wrangler.vitest.jsonc"]) { expect(isConfigFile(path)).toBe(true); @@ -175,9 +230,9 @@ describe("isConfigFile", () => { } }); - it("matches config files by known filename prefix", () => { - for (const path of ["tsconfig.build.json", "vitest.config.ts", ".env.local", ".eslintrc.json", ".prettierrc.js"]) { - expect(isConfigFile(path)).toBe(true); + it("does not classify workflow-like names outside CI directories as config", () => { + for (const path of ["docs/ci.yml", "scripts/release.yaml", "src/workflow.ts"]) { + expect(isConfigFile(path)).toBe(false); } }); @@ -207,10 +262,16 @@ describe("classifyChangedFile", () => { ["src/api.generated.ts", "generated"], ["vendor/lib.go", "vendored"], ["package-lock.json", "lockfile"], + ["deno.lock", "lockfile"], + ["pubspec.lock", "lockfile"], ["package.json", "dependency_manifest"], + ["deno.json", "dependency_manifest"], + ["pubspec.yaml", "dependency_manifest"], ["tsconfig.json", "config"], ["vitest.config.ts", "config"], ["wrangler.jsonc", "config"], + [".github/workflows/ci.yml", "config"], + [".pre-commit-config.yaml", "config"], ["turbo.json", "config"], ["test/unit/app.test.ts", "test"], ["README.md", "docs"],