@varlock/bumpy 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/README.md +256 -0
  2. package/dist/{add-BmNL5VwL.mjs → add-DF6bawDT.mjs} +6 -6
  3. package/dist/{ai-sMYUf3lP.mjs → ai-STKnq09z.mjs} +1 -1
  4. package/dist/{apply-release-plan-0kH62jhu.mjs → apply-release-plan-B1Wwx3HG.mjs} +5 -5
  5. package/dist/{bump-file-DVqR3k67.mjs → bump-file-C3S_bzSf.mjs} +78 -24
  6. package/dist/{changelog-github-DkACMj0j.mjs → changelog-github-DZSHX3Tb.mjs} +20 -5
  7. package/dist/{check-BjWF6SJm.mjs → check-BJL-YDWz.mjs} +38 -11
  8. package/dist/{ci-DY58ugIi.mjs → ci-C88ecvIP.mjs} +115 -33
  9. package/dist/{ci-setup-BQwktQEe.mjs → ci-setup-CARJFhcE.mjs} +1 -1
  10. package/dist/cli.mjs +24 -17
  11. package/dist/{config-B-Qg3DZH.mjs → config-D7Umr-fT.mjs} +3 -3
  12. package/dist/{fs-DYR2XuFE.mjs → fs-DnDogVn-.mjs} +16 -1
  13. package/dist/{generate-DX46X-rW.mjs → generate-D93b3NAD.mjs} +5 -5
  14. package/dist/{git-YDedMddc.mjs → git-H9S9z6g-.mjs} +10 -1
  15. package/dist/index.d.mts +13 -3
  16. package/dist/index.mjs +6 -6
  17. package/dist/{init-DkTPs_WQ.mjs → init-DJhMaceS.mjs} +3 -3
  18. package/dist/{package-manager-Clsmr-9r.mjs → package-manager-ByJ0wKYh.mjs} +1 -1
  19. package/dist/{publish-CGB4TIKD.mjs → publish-DGSV607z.mjs} +6 -6
  20. package/dist/{publish-pipeline-CXuqce1N.mjs → publish-pipeline-DiwZZ5AF.mjs} +3 -3
  21. package/dist/{release-plan-JNir7bSM.mjs → release-plan-CNOuSI-d.mjs} +1 -1
  22. package/dist/{status-EGYqULJg.mjs → status-S2ztf_8E.mjs} +38 -17
  23. package/dist/{version-BcfidiVX.mjs → version-BXrP4TIO.mjs} +11 -7
  24. package/dist/{workspace-DWXlwcH4.mjs → workspace-BHsAPUmC.mjs} +3 -3
  25. package/package.json +2 -1
package/README.md ADDED
@@ -0,0 +1,256 @@
1
+ <p align="center">
2
+ <a href="https://bumpy.varlock.dev" target="_blank" rel="noopener noreferrer">
3
+ <img src="https://raw.githubusercontent.com/dmno-dev/bumpy/refs/heads/main/images/github-readme-banner.png" alt="Bumpy banner">
4
+ </a>
5
+ </p>
6
+ <br/>
7
+ <p align="center">
8
+ <a href="https://npmjs.com/package/@varlock/bumpy"><img src="https://img.shields.io/npm/v/@varlock/bumpy.svg" alt="npm package"></a>
9
+ <a href="https://github.com/dmno-dev/bumpy/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@varlock/bumpy.svg" alt="license"></a>
10
+ <a href="https://github.com/dmno-dev/bumpy/actions/workflows/ci.yaml"><img src="https://img.shields.io/github/actions/workflow/status/dmno-dev/bumpy/ci.yaml?style=flat&logo=github&label=CI" alt="build status"></a>
11
+ <a href="https://chat.dmno.dev"><img src="https://img.shields.io/badge/chat-discord-5865F2?style=flat&logo=discord" alt="discord chat"></a>
12
+ </p>
13
+ <br/>
14
+
15
+ # @varlock/bumpy 🐸
16
+
17
+ A modern package versioning and changelog generation tool — built for monorepos (works great in single packages too).
18
+
19
+ ## How It Works
20
+
21
+ Bumpy uses **bump files** (you may know them as "changesets" if coming from [that tool 🦋](https://github.com/changesets/changesets)) — small markdown files that declare an intent to release packages with a bump level (patch/minor/major), and a description that ends up in changelogs. Developers create these files as part of their PRs, and these files are then used to consolidate changes, generate changelogs, and trigger publishing. Specifically:
22
+
23
+ - Devs/agents create bump files as part of their PRs (using `bumpy add` or manually)
24
+ - A pre-push git hook can enforce bump files exist for changed packages
25
+ - In CI, a workflow checks PRs for bump files, leaves a comment on the PR detailing changed packages
26
+ - As PRs merge to the base branch, a "release PR" is kept up to date
27
+ - Shows what packages will be released and their changelogs
28
+ — Including packages bumped automatically due to dependency relationships
29
+ - When release PR is merged, publishing is triggered
30
+ - Oending bump files are deleted and packages are published with updated versions and changelogs
31
+
32
+ All of this is automated via two simple GitHub Actions workflows (see [CI setup](#ci--github-actions) below). You can also run everything locally with `bumpy status`, `bumpy version`, and `bumpy publish`.
33
+
34
+ ### Example bump file
35
+
36
+ `.bumpy/add-user-language.md`:
37
+
38
+ ```markdown
39
+ ---
40
+ '@myorg/core': minor
41
+ '@myorg/utils': patch
42
+ ---
43
+
44
+ Added user language preference to the core config.
45
+ Fixed locale fallback logic in utils.
46
+ ```
47
+
48
+ ## Features
49
+
50
+ - **All package managers** — npm, pnpm, yarn, and bun workspaces
51
+ - **Smart dependency propagation** — configurable rules for how version bumps cascade through your dependency graph (see [version propagation docs](https://github.com/dmno-dev/bumpy/blob/main/docs/version-propagation.md))
52
+ - **Pack-then-publish** — by default, publishes to npm (resolving `workspace:` and `catalog:` protocols, with OIDC/provenance support). Per-package custom publish commands let you target anything — VSCode extensions, Docker images, JSR, private registries, etc.
53
+ - **Flexible package management** — include/exclude any package individually via per-package config, glob patterns, or `privatePackages` setting
54
+ - **Non-interactive CLI** — `bumpy add` works fully non-interactively for CI/CD and AI-assisted development
55
+ - **Aggregated GitHub releases** — optionally create a single consolidated release instead of one per package
56
+ - **Auto-generate from commits** — `bumpy generate` creates bump files from branch commits — works with any commit style, with enhanced detection for conventional commits
57
+ - **Pluggable changelog formatters** — built-in `"default"` and `"github"` formatters, or write your own
58
+ - **Zero runtime dependencies** — dependencies are minimal and bundled at release time
59
+
60
+ ## Getting Started
61
+
62
+ ```bash
63
+ # Install
64
+ bun add -d @varlock/bumpy # or npm/pnpm/yarn
65
+
66
+ # Initialize (creates .bumpy/ directory and config, migrates from changesets if applicable)
67
+ bunx bumpy init
68
+
69
+ # Create a bump file
70
+ bunx bumpy add
71
+
72
+ # Preview the release plan
73
+ bunx bumpy status
74
+ ```
75
+
76
+ Then set up CI to automate versioning and publishing (see below).
77
+
78
+ ## CI / GitHub Actions
79
+
80
+ No GitHub App to install, no separate action to rely on — just call `bumpy ci` directly in your workflows. Two commands handle the entire release lifecycle:
81
+
82
+ - **`bumpy ci check`** — runs on every PR. Computes the release plan from pending bump files and posts/updates a comment on the PR showing what versions would be released. Warns if any changed packages are missing bump files.
83
+ - **`bumpy ci release`** — runs on push to main. If pending bump files exist, it opens (or updates) a "Version Packages" PR that applies all version bumps and changelog updates. If the current push _is_ the Version Packages PR being merged, it publishes the new versions, creates git tags, and creates GitHub releases.
84
+
85
+ _examples use bun, but works with Node.js_
86
+
87
+ ### PR check workflow
88
+
89
+ ```yaml
90
+ # .github/workflows/bumpy-check.yml
91
+ name: Bumpy Check
92
+ on: pull_request
93
+
94
+ jobs:
95
+ check:
96
+ runs-on: ubuntu-latest
97
+ permissions:
98
+ pull-requests: write
99
+ steps:
100
+ - uses: actions/checkout@v6
101
+ - uses: oven-sh/setup-bun@v2
102
+ - run: bun install
103
+ - run: bunx @varlock/bumpy ci check
104
+ env:
105
+ GH_TOKEN: ${{ github.token }}
106
+ BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }} # additional PAT (optional)
107
+ ```
108
+
109
+ ### Release workflow
110
+
111
+ ```yaml
112
+ # .github/workflows/bumpy-release.yml — trusted publishing (OIDC, no secret needed)
113
+ name: Bumpy Release
114
+ on:
115
+ push:
116
+ branches: [main]
117
+
118
+ jobs:
119
+ release:
120
+ runs-on: ubuntu-latest
121
+ permissions:
122
+ contents: write
123
+ pull-requests: write
124
+ id-token: write # required for npm trusted publishing (OIDC)
125
+ steps:
126
+ - uses: actions/checkout@v6
127
+ with:
128
+ fetch-depth: 0
129
+ - uses: oven-sh/setup-bun@v2
130
+ - uses: actions/setup-node@v6
131
+ with:
132
+ node-version: lts/*
133
+ - run: bun install
134
+ - run: bunx @varlock/bumpy ci release
135
+ env:
136
+ GH_TOKEN: ${{ github.token }}
137
+ BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }} # additonal PAT, needed to trigger CI checks on release PR
138
+ ```
139
+
140
+ > **Trusted publishing setup:** Configure each package on [npmjs.com](https://docs.npmjs.com/trusted-publishers/) → Package Settings → Trusted Publishers → GitHub Actions. Specify your org/user, repo, and the workflow filename (`bumpy-release.yml`). No `NPM_TOKEN` secret needed. Requires npm >= 11.5.1 — bumpy will warn if your version is too old.
141
+
142
+ <details>
143
+ <summary>Alternative: token-based auth (NPM_TOKEN secret)</summary>
144
+
145
+ ```yaml
146
+ # .github/workflows/bumpy-release.yml — token-based auth
147
+ name: Bumpy Release
148
+ on:
149
+ push:
150
+ branches: [main]
151
+
152
+ jobs:
153
+ release:
154
+ runs-on: ubuntu-latest
155
+ permissions:
156
+ contents: write
157
+ pull-requests: write
158
+ steps:
159
+ - uses: actions/checkout@v6
160
+ with:
161
+ fetch-depth: 0
162
+ - uses: oven-sh/setup-bun@v2
163
+ - run: bun install
164
+ - run: bunx @varlock/bumpy ci release
165
+ env:
166
+ GH_TOKEN: ${{ github.token }}
167
+ BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }}
168
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
169
+ ```
170
+
171
+ </details>
172
+
173
+ You can also use `bumpy ci release --auto-publish` to version + publish directly on merge without the intermediate PR.
174
+
175
+ ### Token setup
176
+
177
+ The default `github.token` works for basic functionality, but GitHub's anti-recursion guard means PRs created by the default token won't trigger other workflows — so your regular CI (tests, linting, etc.) won't run automatically on the Version Packages PR. To fix this, provide a `BUMPY_GH_TOKEN` secret using either a **fine-grained PAT** or a **GitHub App token**. See the [full token setup guide](https://github.com/dmno-dev/bumpy/blob/main/docs/github-actions.md#token-setup) for details.
178
+
179
+ Run `bumpy ci setup` for interactive guidance, or set it up manually:
180
+
181
+ 1. Create a [fine-grained personal access token](https://github.com/settings/personal-access-tokens) with:
182
+ - **Repository access:** your repo only
183
+ - **Permissions:** Contents (read & write), Pull requests (read & write)
184
+ 2. Add it as a repository secret named `BUMPY_GH_TOKEN`
185
+ 3. Add it to your release workflow:
186
+ ```yaml
187
+ - run: bunx @varlock/bumpy ci release
188
+ env:
189
+ GH_TOKEN: ${{ github.token }}
190
+ BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }}
191
+ ```
192
+
193
+ ### Local versioning and publishing
194
+
195
+ If you prefer to version and publish locally instead of via CI:
196
+
197
+ ```bash
198
+ bumpy version # consume bump files, update versions and changelogs
199
+ bumpy publish # pack and publish, create git tags, push tags, and create GitHub releases
200
+ ```
201
+
202
+ ## AI Integration
203
+
204
+ Bumpy ships with an AI skill that teaches LLMs how to create bump files.
205
+
206
+ ```bash
207
+ bumpy ai setup --target claude # installs Claude Code plugin
208
+ bumpy ai setup --target opencode # creates OpenCode command file
209
+ bumpy ai setup --target cursor # creates Cursor rule file
210
+ bumpy ai setup --target codex # creates Codex instruction file
211
+ ```
212
+
213
+ The skill teaches the AI to examine git changes, identify affected packages, choose bump levels, and create bump files with `bumpy add`. It also instructs the AI to keep existing bump files up to date as work continues on a branch — updating packages, bump levels, and summaries to reflect the final state of changes.
214
+
215
+ ## Documentation
216
+
217
+ - [Bump file format](https://github.com/dmno-dev/bumpy/blob/main/docs/bump-files.md) — syntax, bump levels, cascade control
218
+ - [Configuration reference](https://github.com/dmno-dev/bumpy/blob/main/docs/configuration.md) — all `.bumpy/_config.json` and per-package options
219
+ - [CLI reference](https://github.com/dmno-dev/bumpy/blob/main/docs/cli.md) — every command with flags and examples
220
+ - [GitHub Actions setup](https://github.com/dmno-dev/bumpy/blob/main/docs/github-actions.md) — CI workflows, token setup, trusted publishing
221
+ - [Version propagation](https://github.com/dmno-dev/bumpy/blob/main/docs/version-propagation.md) — how dependency bumps cascade through your graph
222
+
223
+ ## Why files instead of conventional commits?
224
+
225
+ Tools like semantic-release infer version bumps from commit messages (`feat:` → minor, `fix:` → patch). This works for simple projects but breaks down in monorepos — a single PR often touches multiple packages with different bump levels, squash merges lose per-commit metadata, and commit messages are a poor place to write user-facing changelog entries. Bump files are explicit, reviewable in the PR diff, and can describe changes in language meant for consumers rather than developers. If you prefer commit-based workflows, `bumpy generate` can bridge the gap by auto-creating bump files from your branch commits — it works with any commit style, not just conventional commits.
226
+
227
+ ## Why not just use changesets?
228
+
229
+ Bumpy is built as a successor to [@changesets/changesets](https://github.com/changesets/changesets). Changesets is mature and widely adopted, but has stagnated — hundreds of open issues around core design problems that are unlikely to be fixed without a rewrite. See [differences from changesets](https://github.com/dmno-dev/bumpy/blob/main/docs/differences-from-changesets.md) for a detailed comparison with links to specific issues. The biggest pain points bumpy addresses:
230
+
231
+ - **Sane dependency propagation** — changesets hardcodes aggressive behavior where a minor bump triggers a major bump on all peer dependents. Bumpy uses a [three-phase algorithm](https://github.com/dmno-dev/bumpy/blob/main/docs/version-propagation.md) with sensible defaults and full configurability.
232
+ - **Workspace protocol resolution** — changesets uses `npm publish` even in pnpm/yarn workspaces, so `workspace:^` and `catalog:` protocols are NOT resolved, resulting in broken published packages.
233
+ - **Custom publish commands** — changesets is hardcoded to `npm publish`. Bumpy supports per-package custom publish for VSCode extensions, Docker images, JSR, etc.
234
+ - **Flexible package management** — changesets treats all private packages the same. Bumpy lets you include/exclude any package individually.
235
+ - **CI without a separate action or bot** — changesets requires installing a [GitHub App](https://github.com/apps/changeset-bot) _and_ using a [separate GitHub Action](https://github.com/changesets/action). Bumpy replaces both with two CLI commands (`bumpy ci check` + `bumpy ci release`) that run directly in your workflows — no extra repos to trust, no app installation requiring org admin approval.
236
+ - **Automatic migration** — `bumpy init` detects `.changeset/`, renames it to `.bumpy/`, migrates config, keeps pending files, and offers to uninstall `@changesets/cli`.
237
+
238
+ ## Development
239
+
240
+ ```bash
241
+ bun install # install deps
242
+ bun test # run tests
243
+ bun run build # build CLI
244
+ bunx bumpy --help # invoke built cli
245
+ ```
246
+
247
+ ## Roadmap
248
+
249
+ - Prerelease mode (for now, use [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) for branch preview packages)
250
+ - Standalone binary for use outside of JS projects
251
+ - Better support for versioning non-JS packages and usage without package.json files
252
+ - Plugin system for different publish targets, and support multiple targets per package
253
+ - Tracking workspace-level / non-publishable changes
254
+ - More frogs 🐸
255
+
256
+ <!-- note this readme is also used for the bumpy package! -->
@@ -1,10 +1,10 @@
1
1
  import { n as log, o as __toESM, r as require_picocolors } from "./logger-C2dEe5Su.mjs";
2
- import { n as exists, t as ensureDir } from "./fs-DYR2XuFE.mjs";
3
- import { a as loadConfig, o as loadPackageConfig, r as getBumpyDir, s as matchGlob } from "./config-B-Qg3DZH.mjs";
4
- import { t as discoverPackages } from "./workspace-DWXlwcH4.mjs";
2
+ import { n as exists, t as ensureDir } from "./fs-DnDogVn-.mjs";
3
+ import { a as loadConfig, o as loadPackageConfig, r as getBumpyDir, s as matchGlob } from "./config-D7Umr-fT.mjs";
4
+ import { t as discoverPackages } from "./workspace-BHsAPUmC.mjs";
5
5
  import { t as DependencyGraph } from "./dep-graph-DiLeAhl9.mjs";
6
- import { i as writeBumpFile } from "./bump-file-DVqR3k67.mjs";
7
- import { r as getChangedFiles } from "./git-YDedMddc.mjs";
6
+ import { i as writeBumpFile } from "./bump-file-C3S_bzSf.mjs";
7
+ import { r as getChangedFiles } from "./git-H9S9z6g-.mjs";
8
8
  import { c as ot, d as yt, i as _t, l as pt, o as gt, r as Ot, s as mt, t as unwrap, u as wt } from "./clack-C6bVkGxf.mjs";
9
9
  import { n as slugify, t as randomName } from "./names-C-TuOPbd.mjs";
10
10
  import { t as require_picomatch } from "./picomatch-DMmqYjgq.mjs";
@@ -186,7 +186,7 @@ async function addCommand(rootDir, opts) {
186
186
  if (opts.empty) {
187
187
  const filename = opts.name ? slugify(opts.name) : randomName();
188
188
  const filePath = resolve(bumpyDir, `${filename}.md`);
189
- const { writeText } = await import("./fs-DYR2XuFE.mjs").then((n) => n.r);
189
+ const { writeText } = await import("./fs-DnDogVn-.mjs").then((n) => n.r);
190
190
  await writeText(filePath, "---\n---\n");
191
191
  log.success(`🐸 Created empty bump file: .bumpy/${filename}.md`);
192
192
  return;
@@ -1,5 +1,5 @@
1
1
  import { n as log } from "./logger-C2dEe5Su.mjs";
2
- import { d as writeText, n as exists, t as ensureDir } from "./fs-DYR2XuFE.mjs";
2
+ import { f as writeText, n as exists, t as ensureDir } from "./fs-DnDogVn-.mjs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { execSync } from "node:child_process";
@@ -1,6 +1,6 @@
1
1
  import { n as log } from "./logger-C2dEe5Su.mjs";
2
- import { a as readJson, c as updateJsonFields, d as writeText, i as listFiles, l as updateJsonNestedField, n as exists, o as readText, s as removeFile } from "./fs-DYR2XuFE.mjs";
3
- import { r as getBumpyDir } from "./config-B-Qg3DZH.mjs";
2
+ import { a as readJson, c as removeFile, f as writeText, i as listFiles, l as updateJsonFields, n as exists, s as readText, u as updateJsonNestedField } from "./fs-DnDogVn-.mjs";
3
+ import { r as getBumpyDir } from "./config-D7Umr-fT.mjs";
4
4
  import { relative, resolve } from "node:path";
5
5
  import { realpathSync } from "node:fs";
6
6
  //#region src/core/changelog.ts
@@ -28,7 +28,7 @@ const defaultFormatter = (ctx) => {
28
28
  const BUILTIN_FORMATTERS = {
29
29
  default: defaultFormatter,
30
30
  github: async () => {
31
- const { createGithubFormatter } = await import("./changelog-github-DkACMj0j.mjs");
31
+ const { createGithubFormatter } = await import("./changelog-github-DZSHX3Tb.mjs");
32
32
  return createGithubFormatter();
33
33
  }
34
34
  };
@@ -39,7 +39,7 @@ const BUILTIN_FORMATTERS = {
39
39
  async function loadFormatter(changelog, rootDir) {
40
40
  const [name, options] = Array.isArray(changelog) ? changelog : [changelog, {}];
41
41
  if (name === "github") {
42
- const { createGithubFormatter } = await import("./changelog-github-DkACMj0j.mjs");
42
+ const { createGithubFormatter } = await import("./changelog-github-DZSHX3Tb.mjs");
43
43
  return createGithubFormatter(options);
44
44
  }
45
45
  if (typeof name === "string" && BUILTIN_FORMATTERS[name]) {
@@ -139,7 +139,7 @@ function updateRange(range, newVersion) {
139
139
  cleanRange = range.slice(protocol.length);
140
140
  }
141
141
  const prefix = cleanRange.match(/^(\^|~|>=|>|<=|<|=)?/)?.[1] ?? "^";
142
- if (cleanRange === "*" || cleanRange === "") return range;
142
+ if (cleanRange === "*" || cleanRange === "" || cleanRange === "^" || cleanRange === "~") return range;
143
143
  return `${protocol}${prefix}${newVersion}`;
144
144
  }
145
145
  //#endregion
@@ -1,9 +1,9 @@
1
- import { n as log } from "./logger-C2dEe5Su.mjs";
2
- import { d as writeText, i as listFiles, o as readText } from "./fs-DYR2XuFE.mjs";
3
- import { r as getBumpyDir } from "./config-B-Qg3DZH.mjs";
4
- import { i as jsYaml } from "./package-manager-Clsmr-9r.mjs";
1
+ import { f as writeText, i as listFiles, s as readText } from "./fs-DnDogVn-.mjs";
2
+ import { r as getBumpyDir } from "./config-D7Umr-fT.mjs";
3
+ import { i as jsYaml } from "./package-manager-ByJ0wKYh.mjs";
5
4
  import { s as tryRunArgs } from "./shell-CY7OD48z.mjs";
6
5
  import { resolve } from "node:path";
6
+ import { existsSync } from "node:fs";
7
7
  //#region src/core/bump-file.ts
8
8
  const VALID_BUMP_TYPES = new Set([
9
9
  "major",
@@ -29,16 +29,21 @@ async function readBumpFiles(rootDir) {
29
29
  const dir = getBumpyDir(rootDir);
30
30
  const files = await listFiles(dir, ".md");
31
31
  const bumpFiles = [];
32
+ const errors = [];
32
33
  for (const file of files) {
33
34
  if (file === "README.md") continue;
34
- const bf = await parseBumpFileFromPath(resolve(dir, file));
35
- if (bf) bumpFiles.push(bf);
35
+ const result = await parseBumpFileFromPath(resolve(dir, file));
36
+ if (result.bumpFile) bumpFiles.push(result.bumpFile);
37
+ errors.push(...result.errors);
36
38
  }
37
39
  const creationOrder = getBumpFileCreationOrder(rootDir);
38
40
  if (creationOrder.size > 0) bumpFiles.sort((a, b) => {
39
41
  return (creationOrder.get(a.id) ?? Infinity) - (creationOrder.get(b.id) ?? Infinity) || a.id.localeCompare(b.id);
40
42
  });
41
- return bumpFiles;
43
+ return {
44
+ bumpFiles,
45
+ errors
46
+ };
42
47
  }
43
48
  /**
44
49
  * Use `git log` to get the commit timestamp when each bump file was first added.
@@ -74,21 +79,47 @@ async function parseBumpFileFromPath(filePath) {
74
79
  }
75
80
  /** Parse bump file content (for testing) */
76
81
  function parseBumpFile(content, id) {
77
- const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
78
- if (!match) return null;
82
+ const errors = [];
83
+ const match = content.match(/^---\n([\s\S]*?)\n?---\n?([\s\S]*)$/);
84
+ if (!match) {
85
+ errors.push(`Bump file "${id}" has no valid frontmatter (expected --- delimiters)`);
86
+ return {
87
+ bumpFile: null,
88
+ errors
89
+ };
90
+ }
79
91
  const frontmatter = match[1];
80
92
  const summary = match[2].trim();
81
- const parsed = jsYaml.load(frontmatter);
82
- if (!parsed || typeof parsed !== "object") return null;
93
+ if (!frontmatter.trim()) return {
94
+ bumpFile: null,
95
+ errors
96
+ };
97
+ let parsed;
98
+ try {
99
+ parsed = jsYaml.load(frontmatter);
100
+ } catch (e) {
101
+ errors.push(`Bump file "${id}" has invalid YAML: ${e instanceof Error ? e.message : e}`);
102
+ return {
103
+ bumpFile: null,
104
+ errors
105
+ };
106
+ }
107
+ if (!parsed || typeof parsed !== "object") {
108
+ errors.push(`Bump file "${id}" has empty or invalid frontmatter`);
109
+ return {
110
+ bumpFile: null,
111
+ errors
112
+ };
113
+ }
83
114
  const releases = [];
84
115
  for (const [name, value] of Object.entries(parsed)) {
85
116
  if (!validatePackageName(name)) {
86
- log.warn(`Skipping invalid package name in bump file "${id}": ${name}`);
117
+ errors.push(`Invalid package name "${name}" in bump file "${id}"`);
87
118
  continue;
88
119
  }
89
120
  if (typeof value === "string") {
90
121
  if (!VALID_BUMP_TYPES.has(value)) {
91
- log.warn(`Skipping unknown bump type "${value}" for ${name} in bump file "${id}"`);
122
+ errors.push(`Unknown bump type "${value}" for "${name}" in bump file "${id}" (expected: major, minor, patch, or none)`);
92
123
  continue;
93
124
  }
94
125
  releases.push({
@@ -98,7 +129,7 @@ function parseBumpFile(content, id) {
98
129
  } else if (value && typeof value === "object") {
99
130
  const obj = value;
100
131
  if (!VALID_BUMP_TYPES.has(obj.bump)) {
101
- log.warn(`Skipping unknown bump type "${obj.bump}" for ${name} in bump file "${id}"`);
132
+ errors.push(`Unknown bump type "${obj.bump}" for "${name}" in bump file "${id}" (expected: major, minor, patch, or none)`);
102
133
  continue;
103
134
  }
104
135
  const release = {
@@ -107,13 +138,19 @@ function parseBumpFile(content, id) {
107
138
  cascade: obj.cascade || {}
108
139
  };
109
140
  releases.push(release);
110
- }
141
+ } else errors.push(`Invalid value for "${name}" in bump file "${id}" — expected a bump type string or object`);
111
142
  }
112
- if (releases.length === 0) return null;
143
+ if (releases.length === 0 && errors.length === 0) return {
144
+ bumpFile: null,
145
+ errors
146
+ };
113
147
  return {
114
- id,
115
- releases,
116
- summary
148
+ bumpFile: releases.length > 0 ? {
149
+ id,
150
+ releases,
151
+ summary
152
+ } : null,
153
+ errors
117
154
  };
118
155
  }
119
156
  /** Write a bump file */
@@ -143,14 +180,31 @@ function extractBumpFileIdsFromChangedFiles(changedFiles) {
143
180
  }
144
181
  /**
145
182
  * Filter bump files to only those added/modified on the current branch.
146
- * Returns the filtered bump files and whether any changed bump file was
147
- * empty (has no releases — signals intentionally no releases needed).
183
+ * Also detects empty bump files (no releases) that still exist on disk,
184
+ * which signal intentionally no releases needed.
185
+ *
186
+ * When `parseErrors` is provided, a file that exists on disk but didn't parse
187
+ * is only treated as an "empty bump file" if it produced no parse errors —
188
+ * otherwise it's a broken file, not an intentionally empty one.
148
189
  */
149
- function filterBranchBumpFiles(allBumpFiles, changedFiles) {
190
+ function filterBranchBumpFiles(allBumpFiles, changedFiles, rootDir, parseErrors = []) {
150
191
  const branchBumpFileIds = extractBumpFileIdsFromChangedFiles(changedFiles);
192
+ const branchBumpFiles = allBumpFiles.filter((bf) => branchBumpFileIds.has(bf.id));
193
+ let hasEmptyBumpFile = false;
194
+ if (rootDir) {
195
+ const parsedIds = new Set(branchBumpFiles.map((bf) => bf.id));
196
+ const bumpyDir = getBumpyDir(rootDir);
197
+ for (const id of branchBumpFileIds) if (!parsedIds.has(id) && existsSync(resolve(bumpyDir, `${id}.md`))) {
198
+ if (!parseErrors.some((e) => e.includes(`"${id}"`))) {
199
+ hasEmptyBumpFile = true;
200
+ break;
201
+ }
202
+ }
203
+ }
151
204
  return {
152
- branchBumpFiles: allBumpFiles.filter((bf) => branchBumpFileIds.has(bf.id)),
153
- branchBumpFileIds
205
+ branchBumpFiles,
206
+ branchBumpFileIds,
207
+ hasEmptyBumpFile
154
208
  };
155
209
  }
156
210
  //#endregion
@@ -1,8 +1,22 @@
1
1
  import { s as tryRunArgs } from "./shell-CY7OD48z.mjs";
2
2
  //#region src/core/changelog-github.ts
3
+ /** Authors filtered from "Thanks" attribution by default (e.g. bots) */
4
+ /** Authors filtered from "Thanks" attribution by default (e.g. AI/automation bots) */
5
+ const DEFAULT_INTERNAL_AUTHORS = [
6
+ "copilot",
7
+ "app/copilot-swe-agent",
8
+ "claude",
9
+ "dependabot",
10
+ "dependabot[bot]",
11
+ "app/dependabot",
12
+ "renovate[bot]",
13
+ "app/renovate",
14
+ "github-actions[bot]",
15
+ "snyk-bot"
16
+ ];
3
17
  /**
4
18
  * GitHub-enhanced changelog formatter.
5
- * Adds PR links, commit links, and contributor attribution when git/gh info is available.
19
+ * Adds PR links, contributor attribution, and optionally commit links when git/gh info is available.
6
20
  *
7
21
  * Usage in config:
8
22
  * "changelog": "github"
@@ -11,8 +25,9 @@ import { s as tryRunArgs } from "./shell-CY7OD48z.mjs";
11
25
  * "changelog": ["github", { "internalAuthors": ["theoephraim"] }]
12
26
  */
13
27
  function createGithubFormatter(options = {}) {
28
+ const includeCommitLink = options.includeCommitLink ?? false;
14
29
  const thankContributors = options.thankContributors ?? true;
15
- const internalAuthorsSet = new Set((options.internalAuthors ?? []).map((a) => a.toLowerCase()));
30
+ const internalAuthorsSet = new Set([...DEFAULT_INTERNAL_AUTHORS, ...options.internalAuthors ?? []].map((a) => a.toLowerCase()));
16
31
  return async (ctx) => {
17
32
  const { release, bumpFiles, date } = ctx;
18
33
  const repoSlug = options.repo ?? detectRepo();
@@ -29,7 +44,7 @@ function createGithubFormatter(options = {}) {
29
44
  const gitInfo = resolveBumpFileInfo(bf.id, repoSlug, serverUrl, overrides);
30
45
  const summaryLines = cleanSummary.split("\n");
31
46
  const firstLine = linkifyIssueRefs(summaryLines[0], serverUrl, repoSlug);
32
- const prefix = formatPrefix(gitInfo, serverUrl, repoSlug, thankContributors, internalAuthorsSet);
47
+ const prefix = formatPrefix(gitInfo, serverUrl, repoSlug, includeCommitLink, thankContributors, internalAuthorsSet);
33
48
  lines.push(`-${prefix ? ` ${prefix} -` : ""} ${firstLine}`);
34
49
  for (let i = 1; i < summaryLines.length; i++) if (summaryLines[i].trim()) lines.push(` ${linkifyIssueRefs(summaryLines[i], serverUrl, repoSlug)}`);
35
50
  }
@@ -157,10 +172,10 @@ function findBumpFileCommitInfo(bumpFileId, repo) {
157
172
  * Build the prefix portion of a changelog line: PR link, commit link, thanks.
158
173
  * Matches the format used by @changesets/changelog-github.
159
174
  */
160
- function formatPrefix(info, serverUrl, repo, thankContributors, internalAuthors) {
175
+ function formatPrefix(info, serverUrl, repo, includeCommitLink, thankContributors, internalAuthors) {
161
176
  const parts = [];
162
177
  if (info.prNumber && info.prUrl) parts.push(`[#${info.prNumber}](${info.prUrl})`);
163
- if (info.commitHash && repo) {
178
+ if (includeCommitLink && info.commitHash && repo) {
164
179
  const short = info.commitHash.slice(0, 7);
165
180
  parts.push(`[\`${short}\`](${serverUrl}/${repo}/commit/${info.commitHash})`);
166
181
  }
@@ -1,8 +1,8 @@
1
1
  import { n as log, o as __toESM, t as colorize } from "./logger-C2dEe5Su.mjs";
2
- import { a as loadConfig, o as loadPackageConfig } from "./config-B-Qg3DZH.mjs";
3
- import { n as discoverWorkspace } from "./workspace-DWXlwcH4.mjs";
4
- import { r as readBumpFiles, t as filterBranchBumpFiles } from "./bump-file-DVqR3k67.mjs";
5
- import { r as getChangedFiles } from "./git-YDedMddc.mjs";
2
+ import { a as loadConfig, o as loadPackageConfig, r as getBumpyDir } from "./config-D7Umr-fT.mjs";
3
+ import { n as discoverWorkspace } from "./workspace-BHsAPUmC.mjs";
4
+ import { r as readBumpFiles, t as filterBranchBumpFiles } from "./bump-file-C3S_bzSf.mjs";
5
+ import { r as getChangedFiles } from "./git-H9S9z6g-.mjs";
6
6
  import { t as require_picomatch } from "./picomatch-DMmqYjgq.mjs";
7
7
  import { relative } from "node:path";
8
8
  //#region src/commands/check.ts
@@ -11,8 +11,12 @@ var import_picomatch = /* @__PURE__ */ __toESM(require_picomatch(), 1);
11
11
  * Local check: detect which packages have changed on this branch
12
12
  * and verify they have corresponding bump files.
13
13
  * Designed for pre-push hooks — no GitHub API needed.
14
+ *
15
+ * Default: at least one bump file must exist, uncovered packages are warned.
16
+ * --strict: every changed package must be covered.
17
+ * --no-fail: warn only, never exit 1.
14
18
  */
15
- async function checkCommand(rootDir) {
19
+ async function checkCommand(rootDir, opts = {}) {
16
20
  const config = await loadConfig(rootDir);
17
21
  const { packages } = await discoverWorkspace(rootDir, config);
18
22
  const baseBranch = config.baseBranch;
@@ -21,8 +25,13 @@ async function checkCommand(rootDir) {
21
25
  log.info("No changed files detected.");
22
26
  return;
23
27
  }
24
- const { branchBumpFiles, branchBumpFileIds } = filterBranchBumpFiles(await readBumpFiles(rootDir), changedFiles);
25
- if (branchBumpFileIds.size > branchBumpFiles.length) {
28
+ const { bumpFiles: allBumpFiles, errors: parseErrors } = await readBumpFiles(rootDir);
29
+ if (parseErrors.length > 0) {
30
+ for (const err of parseErrors) log.error(err);
31
+ process.exit(1);
32
+ }
33
+ const { branchBumpFiles, hasEmptyBumpFile } = filterBranchBumpFiles(allBumpFiles, changedFiles, rootDir);
34
+ if (hasEmptyBumpFile) {
26
35
  log.success("Empty bump file found — no releases needed.");
27
36
  return;
28
37
  }
@@ -33,16 +42,34 @@ async function checkCommand(rootDir) {
33
42
  log.info("No managed packages have changed.");
34
43
  return;
35
44
  }
45
+ const willFailNoBump = !opts.noFail;
46
+ if (branchBumpFiles.length === 0) {
47
+ (willFailNoBump ? log.error : log.warn)(`${changedPackages.length} changed package(s) missing bump files:\n`);
48
+ for (const name of changedPackages) console.log(` ${colorize(name, "yellow")}`);
49
+ console.log();
50
+ log.dim("Run `bumpy add` to create a bump file, or `bumpy add --empty` if no release is needed.");
51
+ log.dim("To adjust which files trigger change detection, set `changedFilePatterns` in .bumpy/_config.json.");
52
+ if (willFailNoBump) process.exit(1);
53
+ return;
54
+ }
36
55
  const missing = changedPackages.filter((name) => !coveredPackages.has(name));
37
56
  if (missing.length === 0) {
38
57
  log.success(`🐸 All ${changedPackages.length} changed package(s) have bump files.`);
39
58
  return;
40
59
  }
41
- log.warn(`${missing.length} changed package(s) missing bump files:\n`);
60
+ const willFailUncovered = opts.strict && !opts.noFail;
61
+ (willFailUncovered ? log.error : log.warn)(`${missing.length} changed package(s) missing bump files:\n`);
42
62
  for (const name of missing) console.log(` ${colorize(name, "yellow")}`);
63
+ if (branchBumpFiles.length > 0) {
64
+ console.log();
65
+ log.dim(`Existing bump files on this branch:`);
66
+ for (const bf of branchBumpFiles) log.dim(` ${getBumpyDir(rootDir)}/${bf.id}.md`);
67
+ }
43
68
  console.log();
44
- log.dim("Run `bumpy add` to create a bump file, or `bumpy add --empty` if no release is needed.");
45
- process.exit(1);
69
+ if (opts.strict) log.dim("Run `bumpy add` to create a bump file. Use bump type `none` for packages that changed but don't need a release.");
70
+ else log.dim("Run `bumpy add` to create a bump file, or `bumpy add --empty` if no release is needed.");
71
+ log.dim("To adjust which files trigger change detection, set `changedFilePatterns` in .bumpy/_config.json.");
72
+ if (willFailUncovered) process.exit(1);
46
73
  }
47
74
  /** Map changed files to the packages they belong to */
48
75
  async function findChangedPackages(changedFiles, packages, rootDir, config) {
@@ -62,4 +89,4 @@ async function findChangedPackages(changedFiles, packages, rootDir, config) {
62
89
  return [...changed];
63
90
  }
64
91
  //#endregion
65
- export { checkCommand };
92
+ export { checkCommand, findChangedPackages };