From b76b8e5a621aab0d9d867d54a6be825ab0f8d1f6 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Thu, 18 Jun 2026 12:53:53 -0700 Subject: [PATCH 1/2] fix: detect changes to the root package in single-package repos findChangedPackages matched changed files against pkgRelDir + '/', but the root package has an empty relative dir, making the check file.startsWith('/') which never matches git's relative paths. ci check therefore always reported 'No managed packages have changed' in non-monorepo repos. The root package now treats every changed file as its own (still honoring changedFilePatterns). --- .bumpy/single-package-changed-detection.md | 5 +++ packages/bumpy/src/commands/check.ts | 16 +++++++--- packages/bumpy/test/core/check.test.ts | 37 +++++++++++++++++++++- 3 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 .bumpy/single-package-changed-detection.md diff --git a/.bumpy/single-package-changed-detection.md b/.bumpy/single-package-changed-detection.md new file mode 100644 index 0000000..2712ee9 --- /dev/null +++ b/.bumpy/single-package-changed-detection.md @@ -0,0 +1,5 @@ +--- +'@varlock/bumpy': patch +--- + +Fix changed-package detection in single-package (non-monorepo) repos. `findChangedPackages` matched changed files against `pkgRelDir + '/'`, but for the root package the relative dir is empty, so the check became `file.startsWith('/')` — always false for git's relative paths. As a result `ci check` always reported "No managed packages have changed" and never required a bump file or posted a PR comment. The root package (empty relative dir) now treats every changed file as belonging to it, while still honoring `changedFilePatterns`. diff --git a/packages/bumpy/src/commands/check.ts b/packages/bumpy/src/commands/check.ts index 85623af..ab8646f 100644 --- a/packages/bumpy/src/commands/check.ts +++ b/packages/bumpy/src/commands/check.ts @@ -258,14 +258,22 @@ export async function findChangedPackages( for (const [name, pkg] of packages) { const pkgRelDir = relative(rootDir, pkg.dir); - if (!pkgRelDir) continue; // root-level package: not flagged by file globs (unchanged behavior) + // Root package (single-package repo) has an empty relative dir — every changed + // file belongs to it and is matched directly. Other packages only own files + // under their own directory. + const isRoot = pkgRelDir === ''; const prefix = `${pkgRelDir}/`; const matcher = matchers.get(name)!; let pkgJsonOnlyTrigger = false; for (const file of changedFiles) { - if (!file.startsWith(prefix)) continue; - const relToPackage = file.slice(prefix.length); + let relToPackage: string; + if (isRoot) { + relToPackage = file; + } else { + if (!file.startsWith(prefix)) continue; + relToPackage = file.slice(prefix.length); + } if (!matcher(relToPackage)) continue; if (relToPackage === 'package.json') { pkgJsonOnlyTrigger = true; // defer — refine by inspecting which fields changed @@ -328,7 +336,7 @@ async function packageJsonAffectsRelease( ignoredFields: string[], releaseTriggeringDevDeps?: string[], ): Promise { - const relPath = `${pkgRelDir}/package.json`; + const relPath = pkgRelDir ? `${pkgRelDir}/package.json` : 'package.json'; const beforeRaw = readFileAtRef(rootDir, baseRef, relPath); if (beforeRaw == null) return true; // not present at base (new package) → release-affecting diff --git a/packages/bumpy/test/core/check.test.ts b/packages/bumpy/test/core/check.test.ts index 37799a1..d201111 100644 --- a/packages/bumpy/test/core/check.test.ts +++ b/packages/bumpy/test/core/check.test.ts @@ -2,7 +2,8 @@ import { test, expect, describe, beforeEach, afterEach } from 'bun:test'; import { resolve } from 'node:path'; import { mkdir, writeFile, rm } from 'node:fs/promises'; import { extractBumpFileIdsFromChangedFiles, filterBranchBumpFiles } from '../../src/core/bump-file.ts'; -import { makeBumpFile } from '../helpers.ts'; +import { findChangedPackages } from '../../src/commands/check.ts'; +import { makeBumpFile, makePkg, makeConfig } from '../helpers.ts'; describe('extractBumpFileIdsFromChangedFiles', () => { test('extracts bump file IDs from changed files', () => { @@ -98,3 +99,37 @@ describe('filterBranchBumpFiles', () => { }); }); }); + +describe('findChangedPackages', () => { + const rootDir = '/repo'; + + test('detects the root package in a single-package repo', async () => { + // Single-package repo: the only package IS the root, so pkg.dir === rootDir + // and its relative dir is empty. Every changed file belongs to it. + const packages = new Map([['fledgling', makePkg('fledgling', '1.0.0', { dir: rootDir })]]); + const changed = ['src/cli.ts', 'package.json', 'src/wizard.ts']; + + const result = await findChangedPackages(changed, packages, rootDir, makeConfig()); + expect(result).toEqual(['fledgling']); + }); + + test('respects changedFilePatterns for the root package', async () => { + const packages = new Map([['fledgling', makePkg('fledgling', '1.0.0', { dir: rootDir })]]); + const config = makeConfig({ changedFilePatterns: ['src/**'] }); + + expect(await findChangedPackages(['src/cli.ts'], packages, rootDir, config)).toEqual(['fledgling']); + // A file outside the pattern shouldn't count as a change + expect(await findChangedPackages(['README.md'], packages, rootDir, config)).toEqual([]); + }); + + test('still scopes changes by directory in a monorepo', async () => { + const packages = new Map([ + ['pkg-a', makePkg('pkg-a', '1.0.0', { dir: `${rootDir}/packages/pkg-a` })], + ['pkg-b', makePkg('pkg-b', '1.0.0', { dir: `${rootDir}/packages/pkg-b` })], + ]); + const changed = ['packages/pkg-a/src/index.ts']; + + const result = await findChangedPackages(changed, packages, rootDir, makeConfig()); + expect(result).toEqual(['pkg-a']); + }); +}); From 4999ea467ab437aef57c55210de2cd54dfe341ca Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Thu, 18 Jun 2026 13:47:21 -0700 Subject: [PATCH 2/2] fix: also detect root-package changes in generate command mapFilesToPackages had the same empty-pkgRelDir bug as findChangedPackages: file.startsWith('/') never matches, so 'bumpy generate' never attributed commits to the root package in single-package repos. Export it and cover both single-package and monorepo cases with a test. --- .bumpy/single-package-changed-detection.md | 2 +- packages/bumpy/src/commands/generate.ts | 10 ++++++++-- packages/bumpy/test/core/generate.test.ts | 22 ++++++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 packages/bumpy/test/core/generate.test.ts diff --git a/.bumpy/single-package-changed-detection.md b/.bumpy/single-package-changed-detection.md index 2712ee9..41c35a0 100644 --- a/.bumpy/single-package-changed-detection.md +++ b/.bumpy/single-package-changed-detection.md @@ -2,4 +2,4 @@ '@varlock/bumpy': patch --- -Fix changed-package detection in single-package (non-monorepo) repos. `findChangedPackages` matched changed files against `pkgRelDir + '/'`, but for the root package the relative dir is empty, so the check became `file.startsWith('/')` — always false for git's relative paths. As a result `ci check` always reported "No managed packages have changed" and never required a bump file or posted a PR comment. The root package (empty relative dir) now treats every changed file as belonging to it, while still honoring `changedFilePatterns`. +Fix changed-package detection in single-package (non-monorepo) repos. Both `findChangedPackages` (used by `check`/`ci check`) and `mapFilesToPackages` (used by `generate`) matched changed files against `pkgRelDir + '/'`, but for the root package the relative dir is empty, so the check became `file.startsWith('/')` — always false for git's relative paths. As a result `ci check` always reported "No managed packages have changed" (never requiring a bump file or posting a PR comment) and `generate` never attributed commits to the root package. The root package (empty relative dir) now treats every changed file as belonging to it, while still honoring `changedFilePatterns`. diff --git a/packages/bumpy/src/commands/generate.ts b/packages/bumpy/src/commands/generate.ts index 773619b..6a6c7a7 100644 --- a/packages/bumpy/src/commands/generate.ts +++ b/packages/bumpy/src/commands/generate.ts @@ -191,12 +191,18 @@ function mergeRelease( } /** Map file paths to package names based on directory containment */ -function mapFilesToPackages(files: string[], packages: Map, rootDir: string): string[] { +export function mapFilesToPackages( + files: string[], + packages: Map, + rootDir: string, +): string[] { const matched = new Set(); for (const file of files) { for (const [name, pkg] of packages) { const pkgRelDir = relative(rootDir, pkg.dir); - if (file.startsWith(pkgRelDir + '/')) { + // Root package (single-package repo) has an empty relative dir, so every + // file belongs to it. Otherwise the file must live under the package dir. + if (pkgRelDir === '' || file.startsWith(pkgRelDir + '/')) { matched.add(name); } } diff --git a/packages/bumpy/test/core/generate.test.ts b/packages/bumpy/test/core/generate.test.ts new file mode 100644 index 0000000..eb1c177 --- /dev/null +++ b/packages/bumpy/test/core/generate.test.ts @@ -0,0 +1,22 @@ +import { test, expect, describe } from 'bun:test'; +import { mapFilesToPackages } from '../../src/commands/generate.ts'; +import { makePkg } from '../helpers.ts'; + +describe('mapFilesToPackages', () => { + const rootDir = '/repo'; + + test('attributes every file to the root package in a single-package repo', () => { + const packages = new Map([['fledgling', makePkg('fledgling', '1.0.0', { dir: rootDir })]]); + const result = mapFilesToPackages(['src/cli.ts', 'package.json'], packages, rootDir); + expect(result).toEqual(['fledgling']); + }); + + test('scopes files by directory in a monorepo', () => { + const packages = new Map([ + ['pkg-a', makePkg('pkg-a', '1.0.0', { dir: `${rootDir}/packages/pkg-a` })], + ['pkg-b', makePkg('pkg-b', '1.0.0', { dir: `${rootDir}/packages/pkg-b` })], + ]); + const result = mapFilesToPackages(['packages/pkg-b/src/index.ts'], packages, rootDir); + expect(result).toEqual(['pkg-b']); + }); +});