@varlock/bumpy 1.13.1 → 1.14.0-rc.0

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 (28) hide show
  1. package/README.md +23 -140
  2. package/config-schema.json +43 -0
  3. package/dist/{add-5how2kia.mjs → add-C9rU_89s.mjs} +4 -4
  4. package/dist/{apply-release-plan-DD2R7SL2.mjs → apply-release-plan-DxTsUSqa.mjs} +11 -2
  5. package/dist/{bump-file-B7hmXZlB.mjs → bump-file-mRJeReRJ.mjs} +43 -8
  6. package/dist/{changelog-CbaET5V6.mjs → changelog-DuFhnJRO.mjs} +3 -3
  7. package/dist/{changelog-github-DXDnWkrB.mjs → changelog-github-jLOtwuWj.mjs} +2 -2
  8. package/dist/channels-CFXZkyGd.mjs +75 -0
  9. package/dist/{check-CsF0zh8r.mjs → check-DIl9Dz68.mjs} +18 -6
  10. package/dist/{ci-CIamssoq.mjs → ci-hO7tAbCN.mjs} +391 -23
  11. package/dist/cli.mjs +32 -17
  12. package/dist/{config-D_4GYDJi.mjs → config-0we4ISZX.mjs} +5 -1
  13. package/dist/{generate-CvCvUaRV.mjs → generate-B2OMt_64.mjs} +3 -3
  14. package/dist/{git-DJJ64SW9.mjs → git-DAWj8LyV.mjs} +25 -4
  15. package/dist/index.d.mts +46 -4
  16. package/dist/index.mjs +7 -7
  17. package/dist/prerelease-Blnk8FE1.mjs +186 -0
  18. package/dist/{publish-h6rM58Cq.mjs → publish-Cz0e4KYT.mjs} +164 -19
  19. package/dist/{publish-pipeline-DSj14dW6.mjs → publish-pipeline-BD8mLbL9.mjs} +18 -3
  20. package/dist/{release-plan-mK7iGeGq.mjs → release-plan-C84pcBi-.mjs} +12 -17
  21. package/dist/status-CrMvvvNy.mjs +232 -0
  22. package/dist/{types-Bkh-igOJ.mjs → types-lpiG-Zxh.mjs} +1 -0
  23. package/dist/version-BUUf8vKC.mjs +192 -0
  24. package/package.json +1 -1
  25. package/dist/status-BbsDr6t7.mjs +0 -129
  26. package/dist/version-Cm0nRAFF.mjs +0 -123
  27. /package/dist/{commit-message-CSWVKPJ-.mjs → commit-message-BwsowSds.mjs} +0 -0
  28. /package/dist/{init-BCkm6Nfa.mjs → init-DkAY5hjc.mjs} +0 -0
package/README.md CHANGED
@@ -15,22 +15,21 @@
15
15
 
16
16
  # @varlock/bumpy 🐸
17
17
 
18
- A modern package versioning, release, and changelog generation tool. Built for monorepos, but works great in simpler projects too.
18
+ A modern package versioning, release, and changelog generation tool. Built for monorepos, but works great in simple projects too.
19
19
 
20
20
  ## How It Works
21
21
 
22
- 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
+ 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.
23
23
 
24
24
  - Devs/agents create bump files as part of their PRs (using `bumpy add` or manually)
25
25
  - A git hook (pre-commit or pre-push) can enforce bump files exist for changed packages
26
26
  - In CI, a workflow checks PRs for bump files, leaves a comment on the PR detailing changed packages
27
27
  - As PRs merge to the base branch, a "release PR" is kept up to date
28
- - Shows what packages will be released and their changelogs
29
- - Including packages bumped automatically due to dependency relationships
28
+ - Shows what packages will be released and their changelogs (incl. those bumped via dep relationships)
30
29
  - When release PR is merged, publishing is triggered
31
- - Pending bump files are deleted and packages are published with updated versions and changelogs
30
+ - Pending bump files are deleted and packages are published with updated versions and changelogs, github tags+releases created
32
31
 
33
- 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`.
32
+ All of this is automated via two simple GitHub Actions workflows (see [actions guide](https://github.com/dmno-dev/bumpy/blob/main/docs/github-actions.md)). Or can be triggered locally.
34
33
 
35
34
  ### Example bump file
36
35
 
@@ -42,21 +41,24 @@ All of this is automated via two simple GitHub Actions workflows (see [CI setup]
42
41
  '@myorg/utils': patch
43
42
  ---
44
43
 
45
- Added user language preference to the core config.
44
+ Added user lang prefs to core config.
46
45
  Fixed locale fallback logic in utils.
47
46
  ```
48
47
 
49
48
  ## Features
50
49
 
51
- - **All package managers** - npm, pnpm, yarn, and bun workspaces
50
+ - **All package managers** - npm, pnpm, yarn, and bun workspaces. With full `workspace:` and `catalog:` support
52
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))
53
- - **Pack-then-publish** - by default, publishes to npm (resolving `workspace:` and `catalog:` protocols, with OIDC/provenance support). Supports [npm staged publishing](https://docs.npmjs.com/staged-publishing) for 2FA-gated releases. Per-package custom publish commands let you target anything - VSCode extensions, Docker images, JSR, private registries, etc.
52
+ - **OIDC + staged publishing** - supports OIDC, provenance, [npm staged publishing](https://docs.npmjs.com/staged-publishing)
53
+ - **Custom release targets** - Per-package custom publish commands let you target anything - VSCode extensions, Docker images, JSR, private registries, etc.
54
54
  - **Flexible package management** - include/exclude any package individually via per-package config, glob patterns, or `privatePackages` setting
55
55
  - **Non-interactive CLI** - `bumpy add` works fully non-interactively for CI/CD and AI-assisted development
56
56
  - **Aggregated GitHub releases** - optionally create a single consolidated release instead of one per package
57
+ - **Prerelease channels** - branch-based `@next` / `@beta` release lines where prerelease versions are derived at publish time, never committed to git (see [prerelease channels docs](https://github.com/dmno-dev/bumpy/blob/main/docs/prereleases.md))
57
58
  - **Auto-generate from commits** - `bumpy generate` creates bump files from branch commits - works with any commit style, with enhanced detection for conventional commits
58
59
  - **Pluggable changelog formatters** - built-in `"default"` and `"github"` formatters, or write your own
59
60
  - **Zero runtime dependencies** - dependencies are minimal and bundled at release time
61
+ - **No additional action/app needed** - no external github action or app to audit and trust
60
62
 
61
63
  ## Getting Started
62
64
 
@@ -67,6 +69,9 @@ bun add -d @varlock/bumpy # or npm/pnpm/yarn
67
69
  # Initialize (creates .bumpy/ directory and config, migrates from changesets if applicable)
68
70
  bunx bumpy init
69
71
 
72
+ # Interactive guidance setting up CI
73
+ bunx bumpy ci setup
74
+
70
75
  # Create a bump file
71
76
  bunx bumpy add
72
77
 
@@ -78,138 +83,15 @@ Then set up CI to automate versioning and publishing (see below).
78
83
 
79
84
  ## CI / GitHub Actions
80
85
 
81
- 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:
82
-
83
- - **`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.
84
- - **`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.
85
-
86
- _examples use bun, but works with Node.js_
87
-
88
- ### PR check workflow
89
-
90
- ```yaml
91
- # .github/workflows/bumpy-check.yaml
92
- #
93
- # ⚠️ Uses `pull_request_target` so fork PR comments work — runs with write
94
- # perms and secrets, so it MUST NOT execute PR code (no `bun install`, no
95
- # PR-defined scripts). Bumpy only reads files; its version is resolved from
96
- # the base branch's package.json. See docs/github-actions.md for details.
97
- name: Bumpy Check
98
- on: pull_request_target
99
-
100
- permissions:
101
- pull-requests: write
102
- contents: read
103
-
104
- jobs:
105
- check:
106
- runs-on: ubuntu-latest
107
- steps:
108
- - uses: actions/checkout@v6
109
- with:
110
- ref: ${{ github.event.pull_request.head.sha }}
111
- - uses: oven-sh/setup-bun@v2
112
-
113
- # Resolve bumpy's version from the base branch (trusted) — not the PR's
114
- # package.json (which a fork PR could swap to a malicious version).
115
- # Change "main" to your base branch if different.
116
- - name: Resolve bumpy version from base
117
- run: |
118
- git fetch origin main --depth=1
119
- VERSION=$(git show "origin/main:package.json" \
120
- | jq -r '.devDependencies["@varlock/bumpy"] // .dependencies["@varlock/bumpy"]' \
121
- | sed 's/[\^~]//')
122
- echo "BUMPY_VERSION=$VERSION" >> "$GITHUB_ENV"
123
-
124
- - run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci check
125
- env:
126
- GH_TOKEN: ${{ github.token }}
127
- ```
128
-
129
- ### Release workflow
130
-
131
- ```yaml
132
- # .github/workflows/bumpy-release.yml - trusted publishing (OIDC, no secret needed)
133
- name: Bumpy Release
134
- on:
135
- push:
136
- branches: [main]
137
-
138
- jobs:
139
- release:
140
- runs-on: ubuntu-latest
141
- permissions:
142
- contents: write
143
- pull-requests: write
144
- id-token: write # required for npm trusted publishing (OIDC) and provenance
145
- steps:
146
- - uses: actions/checkout@v6
147
- with:
148
- fetch-depth: 0
149
- - uses: oven-sh/setup-bun@v2
150
- - uses: actions/setup-node@v6
151
- with:
152
- node-version: latest
153
- - run: npm install -g npm@latest # ensure npm >= 11.15.0 for OIDC/staged publishing
154
- - run: bun install
155
- - run: bunx @varlock/bumpy ci release
156
- env:
157
- GH_TOKEN: ${{ github.token }}
158
- BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }} # PAT so that version PR triggers CI
159
- ```
160
-
161
- > **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. Enable `provenance` and `npmStaged` in your [publish config](https://github.com/dmno-dev/bumpy/blob/main/docs/configuration.md#staged-publishing) for maximum security.
162
-
163
- <details>
164
- <summary>Alternative: token-based auth (NPM_TOKEN secret)</summary>
165
-
166
- ```yaml
167
- # .github/workflows/bumpy-release.yml - token-based auth
168
- name: Bumpy Release
169
- on:
170
- push:
171
- branches: [main]
172
-
173
- jobs:
174
- release:
175
- runs-on: ubuntu-latest
176
- permissions:
177
- contents: write
178
- pull-requests: write
179
- steps:
180
- - uses: actions/checkout@v6
181
- with:
182
- fetch-depth: 0
183
- - uses: oven-sh/setup-bun@v2
184
- - run: bun install
185
- - run: bunx @varlock/bumpy ci release
186
- env:
187
- GH_TOKEN: ${{ github.token }}
188
- BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }}
189
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
190
- ```
191
-
192
- </details>
193
-
194
- ### Token setup
195
-
196
- 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.
86
+ No GitHub App to install, no separate action to rely on just call `bumpy ci` directly in your workflows. Three commands across two workflows handle the entire release lifecycle:
197
87
 
198
- Run `bumpy ci setup` for interactive guidance, or set it up manually:
88
+ - **`bumpy ci check`** on every PR, posts/updates a comment showing the release plan and warns if changed packages are missing bump files.
89
+ - **`bumpy ci plan`** — on push to main, detects what should happen next (`version-pr`, `publish`, or nothing) without needing write permissions or publish credentials. Used to gate downstream jobs in split-job workflows.
90
+ - **`bumpy ci release`** — opens/updates the "Version Packages" PR, or publishes new versions and creates git tags + GitHub releases when that PR is merged.
199
91
 
200
- 1. Create a [fine-grained personal access token](https://github.com/settings/personal-access-tokens) with:
201
- - **Repository access:** your repo only
202
- - **Permissions:** Contents (read & write), Pull requests (read & write)
203
- 2. Add it as a repository secret named `BUMPY_GH_TOKEN`
204
- 3. Add it to your release workflow:
205
- ```yaml
206
- - run: bunx @varlock/bumpy ci release
207
- env:
208
- GH_TOKEN: ${{ github.token }}
209
- BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }}
210
- ```
92
+ Run `bumpy ci setup` for interactive guidance, and see the [GitHub Actions setup guide](https://github.com/dmno-dev/bumpy/blob/main/docs/github-actions.md) for ready-to-copy workflows, token setup, and trusted publishing.
211
93
 
212
- ### Local versioning and publishing
94
+ ## Local versioning and publishing
213
95
 
214
96
  If you prefer to version and publish locally instead of via CI:
215
97
 
@@ -238,6 +120,7 @@ The skill teaches the AI to examine git changes, identify affected packages, cho
238
120
  - [CLI reference](https://github.com/dmno-dev/bumpy/blob/main/docs/cli.md) - every command with flags and examples
239
121
  - [GitHub Actions setup](https://github.com/dmno-dev/bumpy/blob/main/docs/github-actions.md) - CI workflows, token setup, trusted publishing
240
122
  - [Version propagation](https://github.com/dmno-dev/bumpy/blob/main/docs/version-propagation.md) - how dependency bumps cascade through your graph
123
+ - [Prerelease channels](https://github.com/dmno-dev/bumpy/blob/main/docs/prereleases.md) - branch-based `@next` / `@beta` release lines
241
124
 
242
125
  ## Why files instead of conventional commits?
243
126
 
@@ -252,6 +135,7 @@ Bumpy is built as a successor to [🦋changesets](https://github.com/changesets/
252
135
  - **Custom publish commands** - changesets is hardcoded to `npm publish`. Bumpy supports per-package custom publish for VSCode extensions, Docker images, JSR, etc.
253
136
  - **Flexible package management** - changesets treats all private packages the same. Bumpy lets you include/exclude any package individually.
254
137
  - **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.
138
+ - **Prerelease channels that don't corrupt state** - changesets' prerelease mode is described in [their own docs](https://github.com/changesets/changesets/blob/main/docs/prereleases.md) as "very complicated" with states "very hard to fix." Bumpy uses [branch-based channels](https://github.com/dmno-dev/bumpy/blob/main/docs/prereleases.md) where prerelease versions are never committed - no global mode file to poison unrelated releases.
255
139
  - **Automatic migration** - `bumpy init` detects `.changeset/`, renames it to `.bumpy/`, migrates config, keeps pending files, and offers to uninstall `@changesets/cli`.
256
140
 
257
141
  ## Development
@@ -265,12 +149,11 @@ bunx bumpy --help # invoke built cli
265
149
 
266
150
  ## Roadmap
267
151
 
268
- - Prerelease mode (for now, use [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) for branch preview packages)
269
152
  - Standalone binary for use outside of JS projects
270
153
  - Better support for versioning non-JS packages and usage without package.json files
271
154
  - Plugin system for different publish targets, and support multiple targets per package
272
155
  - Tracking workspace-level / non-publishable changes
273
- - More frogs 🐸
156
+ - More frogs 🐸🐸🐸
274
157
 
275
158
  ---
276
159
 
@@ -202,6 +202,49 @@
202
202
  }
203
203
  },
204
204
  "additionalProperties": false
205
+ },
206
+ "channels": {
207
+ "type": "object",
208
+ "description": "Prerelease channels, keyed by channel name. Each maps a long-lived branch to a prerelease line (version suffix + npm dist-tag). Prerelease versions are derived at publish time and never committed.",
209
+ "additionalProperties": {
210
+ "type": "object",
211
+ "properties": {
212
+ "branch": {
213
+ "type": "string",
214
+ "description": "Branch that triggers this channel (required)"
215
+ },
216
+ "preid": {
217
+ "type": "string",
218
+ "description": "Version suffix (preid), e.g. \"rc\" produces 1.2.0-rc.0. Defaults to the channel name."
219
+ },
220
+ "tag": {
221
+ "type": "string",
222
+ "description": "npm dist-tag for publishes. Defaults to the channel name."
223
+ },
224
+ "versionPr": {
225
+ "type": "object",
226
+ "description": "Release PR overrides for this channel",
227
+ "properties": {
228
+ "title": {
229
+ "type": "string",
230
+ "description": "Release PR title. Defaults to \"<base title> (<channel name>)\"."
231
+ },
232
+ "branch": {
233
+ "type": "string",
234
+ "description": "Release PR branch. Defaults to \"<base branch>-<channel name>\"."
235
+ },
236
+ "automerge": {
237
+ "type": "boolean",
238
+ "description": "Enable auto-merge on the release PR",
239
+ "default": false
240
+ }
241
+ },
242
+ "additionalProperties": false
243
+ }
244
+ },
245
+ "required": ["branch"],
246
+ "additionalProperties": false
247
+ }
205
248
  }
206
249
  },
207
250
  "additionalProperties": false,
@@ -1,11 +1,11 @@
1
1
  import { n as log, r as require_picocolors, s as __toESM } from "./logger-BgksGFuf.mjs";
2
2
  import { n as exists, t as ensureDir } from "./fs-CBXKZhoU.mjs";
3
- import { a as loadConfig, r as getBumpyDir } from "./config-D_4GYDJi.mjs";
4
- import { a as writeBumpFile, r as readBumpFiles, s as discoverWorkspace, t as filterBranchBumpFiles } from "./bump-file-B7hmXZlB.mjs";
5
- import { i as getChangedFiles } from "./git-DJJ64SW9.mjs";
3
+ import { a as loadConfig, r as getBumpyDir } from "./config-0we4ISZX.mjs";
4
+ import { c as discoverWorkspace, i as readBumpFiles, o as writeBumpFile, t as filterBranchBumpFiles } from "./bump-file-mRJeReRJ.mjs";
5
+ import { a as getChangedFiles } from "./git-DAWj8LyV.mjs";
6
6
  import { l as pt, o as gt, r as Ot, s as mt, t as unwrap, u as wt } from "./clack-W95rXis0.mjs";
7
7
  import { n as slugify, t as randomName } from "./names-COooXAFg.mjs";
8
- import { n as findChangedPackages } from "./check-CsF0zh8r.mjs";
8
+ import { n as findChangedPackages } from "./check-DIl9Dz68.mjs";
9
9
  import { resolve } from "node:path";
10
10
  import * as readline from "node:readline";
11
11
  //#region src/prompts/bump-select.ts
@@ -1,6 +1,6 @@
1
1
  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-CBXKZhoU.mjs";
2
- import { r as getBumpyDir } from "./config-D_4GYDJi.mjs";
3
- import { a as prependToChangelog, i as loadFormatter, n as generateChangelogEntry } from "./changelog-CbaET5V6.mjs";
2
+ import { r as getBumpyDir } from "./config-0we4ISZX.mjs";
3
+ import { a as prependToChangelog, i as loadFormatter, n as generateChangelogEntry } from "./changelog-DuFhnJRO.mjs";
4
4
  import { resolve } from "node:path";
5
5
  //#region src/core/apply-release-plan.ts
6
6
  /** Apply the release plan: bump versions, update changelogs, delete bump files */
@@ -39,6 +39,15 @@ async function applyReleasePlan(releasePlan, packages, rootDir, config) {
39
39
  if (file === "README.md") continue;
40
40
  await removeFile(resolve(bumpyDir, file));
41
41
  }
42
+ const { rmdir } = await import("node:fs/promises");
43
+ for (const channel of Object.keys(config.channels || {})) {
44
+ const channelDir = resolve(bumpyDir, channel);
45
+ const channelFiles = await listFiles(channelDir, ".md");
46
+ for (const file of channelFiles) await removeFile(resolve(channelDir, file));
47
+ try {
48
+ await rmdir(channelDir);
49
+ } catch {}
50
+ }
42
51
  }
43
52
  /** Update a version range to include a new version, preserving the range prefix */
44
53
  function updateRange(range, newVersion) {
@@ -1,5 +1,5 @@
1
1
  import { a as readJson, f as writeText, i as listFiles, n as exists, s as readText } from "./fs-CBXKZhoU.mjs";
2
- import { i as isPackageManaged, o as loadPackageConfig, r as getBumpyDir } from "./config-D_4GYDJi.mjs";
2
+ import { i as isPackageManaged, o as loadPackageConfig, r as getBumpyDir } from "./config-0we4ISZX.mjs";
3
3
  import { c as jsYaml, r as detectWorkspaces } from "./package-manager-Db_vTztt.mjs";
4
4
  import { s as tryRunArgs } from "./shell-C8KgKnMQ.mjs";
5
5
  import { relative, resolve } from "node:path";
@@ -128,18 +128,38 @@ function validatePackageName(name) {
128
128
  if (name.startsWith("-")) return false;
129
129
  return true;
130
130
  }
131
- /** Read all bump files from .bumpy/ directory, sorted by git creation order */
132
- async function readBumpFiles(rootDir) {
131
+ /** Read all bump files from .bumpy/ (and optionally channel subdirs), sorted by git creation order */
132
+ async function readBumpFiles(rootDir, opts = {}) {
133
133
  const dir = getBumpyDir(rootDir);
134
- const files = await listFiles(dir, ".md");
135
134
  const bumpFiles = [];
136
135
  const errors = [];
136
+ const files = await listFiles(dir, ".md");
137
137
  for (const file of files) {
138
138
  if (file === "README.md") continue;
139
139
  const result = await parseBumpFileFromPath(resolve(dir, file));
140
140
  if (result.bumpFile) bumpFiles.push(result.bumpFile);
141
141
  errors.push(...result.errors);
142
142
  }
143
+ for (const channel of opts.channels ?? []) {
144
+ const channelDir = resolve(dir, channel);
145
+ const channelFiles = await listFiles(channelDir, ".md");
146
+ for (const file of channelFiles) {
147
+ if (file === "README.md") continue;
148
+ const result = await parseBumpFileFromPath(resolve(channelDir, file));
149
+ if (result.bumpFile) {
150
+ const duplicate = bumpFiles.find((bf) => bf.id === result.bumpFile.id);
151
+ if (duplicate) {
152
+ errors.push(`Bump file "${result.bumpFile.id}" exists both ${duplicate.channel ? `in .bumpy/${duplicate.channel}/` : "at .bumpy/ root"} and in .bumpy/${channel}/ — remove one copy (the change likely already shipped on one of them).`);
153
+ continue;
154
+ }
155
+ bumpFiles.push({
156
+ ...result.bumpFile,
157
+ channel
158
+ });
159
+ }
160
+ errors.push(...result.errors);
161
+ }
162
+ }
143
163
  const creationOrder = getBumpFileCreationOrder(rootDir);
144
164
  if (creationOrder.size > 0) bumpFiles.sort((a, b) => {
145
165
  return (creationOrder.get(a.id) ?? Infinity) - (creationOrder.get(b.id) ?? Infinity) || a.id.localeCompare(b.id);
@@ -171,7 +191,7 @@ function getBumpFileCreationOrder(rootDir) {
171
191
  if (!trimmed) continue;
172
192
  if (/^\d+$/.test(trimmed)) currentTimestamp = parseInt(trimmed, 10);
173
193
  else if (trimmed.startsWith(".bumpy/") && trimmed.endsWith(".md")) {
174
- const id = trimmed.replace(/^\.bumpy\//, "").replace(/\.md$/, "");
194
+ const id = fileToId(trimmed);
175
195
  order.set(id, currentTimestamp);
176
196
  }
177
197
  }
@@ -298,11 +318,26 @@ function recoverDeletedBumpFiles(rootDir) {
298
318
  `HEAD~1:${filePath}`
299
319
  ], { cwd: rootDir });
300
320
  if (!content) continue;
301
- const { bumpFile } = parseBumpFile(content, filePath.replace(/^\.bumpy\//, "").replace(/\.md$/, ""));
321
+ const { bumpFile } = parseBumpFile(content, fileToId(filePath));
302
322
  if (bumpFile) bumpFiles.push(bumpFile);
303
323
  }
304
324
  return bumpFiles;
305
325
  }
326
+ /**
327
+ * Move bump files into a channel's shipped directory (`.bumpy/<channel>/`).
328
+ * This is the only thing a channel "version" does — prerelease versions are
329
+ * never written to git. Files already in the target channel dir are left alone.
330
+ */
331
+ async function moveBumpFilesToChannel(rootDir, bumpFiles, channel) {
332
+ const { rename, mkdir } = await import("node:fs/promises");
333
+ const dir = getBumpyDir(rootDir);
334
+ const channelDir = resolve(dir, channel);
335
+ await mkdir(channelDir, { recursive: true });
336
+ for (const bf of bumpFiles) {
337
+ if (bf.channel === channel) continue;
338
+ await rename(bf.channel ? resolve(dir, bf.channel, `${bf.id}.md`) : resolve(dir, `${bf.id}.md`), resolve(channelDir, `${bf.id}.md`));
339
+ }
340
+ }
306
341
  function fileToId(filePath) {
307
342
  return filePath.split("/").pop().replace(/\.md$/, "");
308
343
  }
@@ -311,7 +346,7 @@ function fileToId(filePath) {
311
346
  * of bump files that were added/modified. Shared by `check` and `ci check`.
312
347
  */
313
348
  function extractBumpFileIdsFromChangedFiles(changedFiles) {
314
- return new Set(changedFiles.filter((f) => /^\.bumpy\/.*\.md$/.test(f) && !f.endsWith("README.md")).map((f) => f.replace(/^\.bumpy\//, "").replace(/\.md$/, "")));
349
+ return new Set(changedFiles.filter((f) => /^\.bumpy\/.*\.md$/.test(f) && !f.endsWith("README.md")).map(fileToId));
315
350
  }
316
351
  /**
317
352
  * Filter bump files to only those added/modified on the current branch.
@@ -340,4 +375,4 @@ function filterBranchBumpFiles(allBumpFiles, changedFiles, rootDir, parseErrors
340
375
  };
341
376
  }
342
377
  //#endregion
343
- export { writeBumpFile as a, recoverDeletedBumpFiles as i, parseBumpFile as n, discoverPackages as o, readBumpFiles as r, discoverWorkspace as s, filterBranchBumpFiles as t };
378
+ export { recoverDeletedBumpFiles as a, discoverWorkspace as c, readBumpFiles as i, moveBumpFilesToChannel as n, writeBumpFile as o, parseBumpFile as r, discoverPackages as s, filterBranchBumpFiles as t };
@@ -1,5 +1,5 @@
1
1
  import { n as log } from "./logger-BgksGFuf.mjs";
2
- import { c as maxBump, t as BUMP_LEVELS } from "./types-Bkh-igOJ.mjs";
2
+ import { c as maxBump, t as BUMP_LEVELS } from "./types-lpiG-Zxh.mjs";
3
3
  import { relative, resolve } from "node:path";
4
4
  import { realpathSync } from "node:fs";
5
5
  //#region src/core/changelog.ts
@@ -44,7 +44,7 @@ const defaultFormatter = (ctx) => {
44
44
  const BUILTIN_FORMATTERS = {
45
45
  default: defaultFormatter,
46
46
  github: async () => {
47
- const { createGithubFormatter } = await import("./changelog-github-DXDnWkrB.mjs");
47
+ const { createGithubFormatter } = await import("./changelog-github-jLOtwuWj.mjs");
48
48
  return createGithubFormatter();
49
49
  }
50
50
  };
@@ -55,7 +55,7 @@ const BUILTIN_FORMATTERS = {
55
55
  async function loadFormatter(changelog, rootDir) {
56
56
  const [name, options] = Array.isArray(changelog) ? changelog : [changelog, {}];
57
57
  if (name === "github") {
58
- const { createGithubFormatter } = await import("./changelog-github-DXDnWkrB.mjs");
58
+ const { createGithubFormatter } = await import("./changelog-github-jLOtwuWj.mjs");
59
59
  return createGithubFormatter(options);
60
60
  }
61
61
  if (typeof name === "string" && BUILTIN_FORMATTERS[name]) {
@@ -1,6 +1,6 @@
1
- import { c as maxBump } from "./types-Bkh-igOJ.mjs";
1
+ import { c as maxBump } from "./types-lpiG-Zxh.mjs";
2
2
  import { s as tryRunArgs } from "./shell-C8KgKnMQ.mjs";
3
- import { o as sortBumpFilesByType, r as getBumpTypeForPackage } from "./changelog-CbaET5V6.mjs";
3
+ import { o as sortBumpFilesByType, r as getBumpTypeForPackage } from "./changelog-DuFhnJRO.mjs";
4
4
  //#region src/core/changelog-github.ts
5
5
  /** Authors filtered from "Thanks" attribution by default (e.g. bots) */
6
6
  /** Authors filtered from "Thanks" attribution by default (e.g. AI/automation bots) */
@@ -0,0 +1,75 @@
1
+ import "./config-0we4ISZX.mjs";
2
+ import { o as getCurrentBranch } from "./git-DAWj8LyV.mjs";
3
+ import "node:path";
4
+ //#region src/core/channels.ts
5
+ /** Channel names that would collide with reserved `.bumpy/` entries */
6
+ const RESERVED_CHANNEL_NAMES = new Set(["README", "README.md"]);
7
+ /**
8
+ * Resolve all configured channels, applying defaults and validating names.
9
+ * Defaults: preid/tag = channel name; versionPr.title = "<base-title> (<name>)";
10
+ * versionPr.branch = "<base-branch>-<name>".
11
+ */
12
+ function resolveChannels(config) {
13
+ const channels = /* @__PURE__ */ new Map();
14
+ const seenBranches = /* @__PURE__ */ new Map();
15
+ for (const [name, raw] of Object.entries(config.channels || {})) {
16
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name) || name.startsWith("_") || RESERVED_CHANNEL_NAMES.has(name)) throw new Error(`Invalid channel name "${name}" — channel names become .bumpy/ subdirectories and must be alphanumeric (plus ".", "-", "_"), not start with "_", and not collide with reserved entries.`);
17
+ if (!raw.branch || typeof raw.branch !== "string") throw new Error(`Channel "${name}" is missing required "branch" field`);
18
+ if (raw.branch === config.baseBranch) throw new Error(`Channel "${name}" cannot use the base branch ("${config.baseBranch}") as its channel branch`);
19
+ const existing = seenBranches.get(raw.branch);
20
+ if (existing) throw new Error(`Channels "${existing}" and "${name}" both use branch "${raw.branch}"`);
21
+ seenBranches.set(raw.branch, name);
22
+ channels.set(name, {
23
+ name,
24
+ branch: raw.branch,
25
+ preid: raw.preid ?? name,
26
+ tag: raw.tag ?? name,
27
+ versionPr: {
28
+ title: raw.versionPr?.title ?? `${config.versionPr.title} (${name})`,
29
+ branch: raw.versionPr?.branch ?? `${config.versionPr.branch}-${name}`,
30
+ automerge: raw.versionPr?.automerge ?? false
31
+ }
32
+ });
33
+ }
34
+ return channels;
35
+ }
36
+ /** Names of all configured channels (used as `.bumpy/` subdirectory names) */
37
+ function channelNames(config) {
38
+ return Object.keys(config.channels || {});
39
+ }
40
+ /**
41
+ * Detect the branch the release flow is running for.
42
+ * In GitHub Actions push events, HEAD is often detached — prefer GITHUB_REF_NAME.
43
+ */
44
+ function detectReleaseBranch(rootDir) {
45
+ const refName = process.env.GITHUB_REF_NAME;
46
+ const refType = process.env.GITHUB_REF_TYPE;
47
+ if (refName && refType !== "tag") return refName;
48
+ const branch = getCurrentBranch({ cwd: rootDir });
49
+ if (!branch || branch === "HEAD") return null;
50
+ return branch;
51
+ }
52
+ /** Find the channel matching a branch name, if any */
53
+ function matchChannelByBranch(config, branch) {
54
+ if (!branch) return null;
55
+ for (const channel of resolveChannels(config).values()) if (channel.branch === branch) return channel;
56
+ return null;
57
+ }
58
+ /**
59
+ * Resolve the active channel for a command:
60
+ * an explicit `--channel <name>` override wins, otherwise the current branch is matched.
61
+ * Throws if an explicit override names an unknown channel.
62
+ */
63
+ function resolveActiveChannel(rootDir, config, override) {
64
+ if (override) {
65
+ const channel = resolveChannels(config).get(override);
66
+ if (!channel) {
67
+ const known = channelNames(config);
68
+ throw new Error(`Unknown channel "${override}"${known.length ? ` — configured channels: ${known.join(", ")}` : " — no channels are configured in .bumpy/_config.json"}`);
69
+ }
70
+ return channel;
71
+ }
72
+ return matchChannelByBranch(config, detectReleaseBranch(rootDir));
73
+ }
74
+ //#endregion
75
+ export { channelNames, detectReleaseBranch, matchChannelByBranch, resolveActiveChannel, resolveChannels };
@@ -1,10 +1,10 @@
1
1
  import { a as __exportAll, i as __commonJSMin, n as log, s as __toESM, t as colorize } from "./logger-BgksGFuf.mjs";
2
2
  import { n as exists, s as readText } from "./fs-CBXKZhoU.mjs";
3
- import { a as DEP_TYPES } from "./types-Bkh-igOJ.mjs";
4
- import { a as loadConfig, o as loadPackageConfig, r as getBumpyDir } from "./config-D_4GYDJi.mjs";
3
+ import { a as DEP_TYPES } from "./types-lpiG-Zxh.mjs";
4
+ import { a as loadConfig, o as loadPackageConfig, r as getBumpyDir } from "./config-0we4ISZX.mjs";
5
5
  import { a as isCatalogRefAffected, i as diffCatalogMaps, n as detectPackageManager, o as parseCatalogs, t as CATALOG_FILES } from "./package-manager-Db_vTztt.mjs";
6
- import { r as readBumpFiles, s as discoverWorkspace, t as filterBranchBumpFiles } from "./bump-file-B7hmXZlB.mjs";
7
- import { i as getChangedFiles, n as getBaseCompareRef, o as getFileStatuses, u as readFileAtRef } from "./git-DJJ64SW9.mjs";
6
+ import { c as discoverWorkspace, i as readBumpFiles, t as filterBranchBumpFiles } from "./bump-file-mRJeReRJ.mjs";
7
+ import { a as getChangedFiles, d as readFileAtRef, r as getBaseCompareRef, s as getFileStatuses } from "./git-DAWj8LyV.mjs";
8
8
  import { relative, resolve } from "node:path";
9
9
  //#region ../../node_modules/.bun/picomatch@4.0.4/node_modules/picomatch/lib/constants.js
10
10
  var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -1894,8 +1894,20 @@ var import_picomatch = /* @__PURE__ */ __toESM(require_picomatch(), 1);
1894
1894
  async function checkCommand(rootDir, opts = {}) {
1895
1895
  const config = await loadConfig(rootDir);
1896
1896
  const { packages } = await discoverWorkspace(rootDir, config);
1897
- const baseBranch = config.baseBranch;
1898
- const changedFiles = getChangedFiles(rootDir, baseBranch);
1897
+ const { resolveChannels, detectReleaseBranch } = await import("./channels-CFXZkyGd.mjs");
1898
+ const currentBranch = detectReleaseBranch(rootDir);
1899
+ if (currentBranch) {
1900
+ const skipBranches = new Set([config.versionPr.branch]);
1901
+ for (const channel of resolveChannels(config).values()) {
1902
+ skipBranches.add(channel.branch);
1903
+ skipBranches.add(channel.versionPr.branch);
1904
+ }
1905
+ if (skipBranches.has(currentBranch)) {
1906
+ log.dim(` Skipping check — "${currentBranch}" is a channel or release PR branch.`);
1907
+ return;
1908
+ }
1909
+ }
1910
+ const changedFiles = getChangedFiles(rootDir, opts.base || config.baseBranch);
1899
1911
  if (changedFiles.length === 0) {
1900
1912
  log.info("No changed files detected.");
1901
1913
  return;