@varlock/bumpy 1.13.0 → 1.13.2

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.
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,23 @@ 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
57
  - **Auto-generate from commits** - `bumpy generate` creates bump files from branch commits - works with any commit style, with enhanced detection for conventional commits
58
58
  - **Pluggable changelog formatters** - built-in `"default"` and `"github"` formatters, or write your own
59
59
  - **Zero runtime dependencies** - dependencies are minimal and bundled at release time
60
+ - **No additional action/app needed** - no external github action or app to audit and trust
60
61
 
61
62
  ## Getting Started
62
63
 
@@ -67,6 +68,9 @@ bun add -d @varlock/bumpy # or npm/pnpm/yarn
67
68
  # Initialize (creates .bumpy/ directory and config, migrates from changesets if applicable)
68
69
  bunx bumpy init
69
70
 
71
+ # Interactive guidance setting up CI
72
+ bunx bumpy ci setup
73
+
70
74
  # Create a bump file
71
75
  bunx bumpy add
72
76
 
@@ -78,138 +82,15 @@ Then set up CI to automate versioning and publishing (see below).
78
82
 
79
83
  ## CI / GitHub Actions
80
84
 
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.
85
+ 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
86
 
198
- Run `bumpy ci setup` for interactive guidance, or set it up manually:
87
+ - **`bumpy ci check`** on every PR, posts/updates a comment showing the release plan and warns if changed packages are missing bump files.
88
+ - **`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.
89
+ - **`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
90
 
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
- ```
91
+ 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
92
 
212
- ### Local versioning and publishing
93
+ ## Local versioning and publishing
213
94
 
214
95
  If you prefer to version and publish locally instead of via CI:
215
96
 
@@ -270,7 +151,7 @@ bunx bumpy --help # invoke built cli
270
151
  - Better support for versioning non-JS packages and usage without package.json files
271
152
  - Plugin system for different publish targets, and support multiple targets per package
272
153
  - Tracking workspace-level / non-publishable changes
273
- - More frogs 🐸
154
+ - More frogs 🐸🐸🐸
274
155
 
275
156
  ---
276
157
 
@@ -2,10 +2,10 @@ import { n as log, r as require_picocolors, s as __toESM } from "./logger-BgksGF
2
2
  import { n as exists, t as ensureDir } from "./fs-CBXKZhoU.mjs";
3
3
  import { a as loadConfig, r as getBumpyDir } from "./config-D_4GYDJi.mjs";
4
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";
5
+ import { a as getChangedFiles } from "./git-BWPimLgc.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-Dvi0DIqC.mjs";
9
9
  import { resolve } from "node:path";
10
10
  import * as readline from "node:readline";
11
11
  //#region src/prompts/bump-select.ts
@@ -24,6 +24,10 @@ const LEVELS = [
24
24
  * - Changed packages default to "patch", unchanged to "none"
25
25
  * - Enter to confirm
26
26
  * - Ctrl+C / Escape to cancel
27
+ *
28
+ * Renders a viewport that fits within the terminal so the list scrolls instead of
29
+ * overflowing — otherwise large package counts cause the redraw cursor-up to lose
30
+ * its anchor once content scrolls off-screen.
27
31
  */
28
32
  async function bumpSelectPrompt(items) {
29
33
  const changedEntries = items.map((item, idx) => ({
@@ -35,7 +39,44 @@ async function bumpSelectPrompt(items) {
35
39
  idx
36
40
  })).filter(({ item }) => !item.changed);
37
41
  const displayOrder = [...changedEntries, ...unchangedEntries];
42
+ const rows = [];
43
+ const itemRowIndex = [];
44
+ {
45
+ let displayIdx = 0;
46
+ if (changedEntries.length > 0) {
47
+ rows.push({
48
+ kind: "header",
49
+ text: "Changed"
50
+ });
51
+ for (const { idx } of changedEntries) {
52
+ itemRowIndex.push(rows.length);
53
+ rows.push({
54
+ kind: "item",
55
+ itemIdx: idx,
56
+ displayIdx
57
+ });
58
+ displayIdx++;
59
+ }
60
+ if (unchangedEntries.length > 0) rows.push({ kind: "separator" });
61
+ }
62
+ if (unchangedEntries.length > 0) {
63
+ rows.push({
64
+ kind: "header",
65
+ text: "Unchanged"
66
+ });
67
+ for (const { idx } of unchangedEntries) {
68
+ itemRowIndex.push(rows.length);
69
+ rows.push({
70
+ kind: "item",
71
+ itemIdx: idx,
72
+ displayIdx
73
+ });
74
+ displayIdx++;
75
+ }
76
+ }
77
+ }
38
78
  let cursor = 0;
79
+ let scroll = 0;
39
80
  const levels = items.map((item) => item.initialLevel !== void 0 ? item.initialLevel : item.changed ? "patch" : "skip");
40
81
  return new Promise((resolve) => {
41
82
  const { stdin, stdout } = process;
@@ -57,38 +98,77 @@ async function bumpSelectPrompt(items) {
57
98
  if (selected.length === 0) lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.dim("(none selected)")}`);
58
99
  else for (const { item, idx } of selected) lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.cyan(item.name)} ${import_picocolors.default.dim("→")} ${import_picocolors.default.bold(levels[idx])}`);
59
100
  lines.push(import_picocolors.default.dim("│"));
101
+ const output = lines.join("\n") + "\n";
102
+ stdout.write(output);
103
+ renderedLines = lines.length;
104
+ return;
105
+ }
106
+ const headerChrome = [
107
+ `${import_picocolors.default.cyan("◆")} Select bump levels`,
108
+ `${import_picocolors.default.dim("│")} ${import_picocolors.default.dim("↑/↓ navigate · ←/→ change level · enter to confirm")}`,
109
+ `${import_picocolors.default.dim("│")} ${import_picocolors.default.dim("0 skip current · x skip all · r reset all to defaults")}`,
110
+ import_picocolors.default.dim("│")
111
+ ];
112
+ const selectedCount = levels.filter((l) => l !== "skip").length;
113
+ const footerChrome = [
114
+ import_picocolors.default.dim("│"),
115
+ `${import_picocolors.default.dim("│")} ${import_picocolors.default.dim(`${selectedCount} package${selectedCount !== 1 ? "s" : ""} selected`)}`,
116
+ import_picocolors.default.dim("└")
117
+ ];
118
+ const termRows = stdout.rows || 24;
119
+ const chromeLines = headerChrome.length + footerChrome.length;
120
+ const MIN_BODY = 3;
121
+ const availableBody = Math.max(MIN_BODY, termRows - chromeLines - 1);
122
+ let visibleRows;
123
+ let topIndicator = null;
124
+ let bottomIndicator = null;
125
+ let stickyHeader = null;
126
+ if (rows.length <= availableBody) {
127
+ visibleRows = rows;
128
+ scroll = 0;
60
129
  } else {
61
- lines.push(`${import_picocolors.default.cyan("◆")} Select bump levels`);
62
- lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.dim("↑/↓ navigate · ←/→ change level · enter to confirm")}`);
63
- lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.dim("0 skip current · x skip all · r reset all to defaults")}`);
64
- lines.push(import_picocolors.default.dim("│"));
65
- let displayIdx = 0;
66
- if (changedEntries.length > 0) {
67
- lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.underline("Changed")}`);
68
- for (const { item, idx } of changedEntries) {
69
- lines.push(formatRow(item, levels[idx], cursor === displayIdx));
70
- displayIdx++;
71
- }
72
- if (unchangedEntries.length > 0) lines.push(import_picocolors.default.dim("│"));
130
+ let windowSize = Math.max(MIN_BODY, availableBody - 2);
131
+ const focusedRowIdx = itemRowIndex[cursor];
132
+ const adjustScroll = () => {
133
+ if (focusedRowIdx < scroll) scroll = focusedRowIdx;
134
+ else if (focusedRowIdx >= scroll + windowSize) scroll = focusedRowIdx - windowSize + 1;
135
+ scroll = Math.max(0, Math.min(scroll, rows.length - windowSize));
136
+ };
137
+ adjustScroll();
138
+ const section = getCurrentSection(cursor, changedEntries.length, unchangedEntries.length);
139
+ if (section !== null && section.headerRowIdx < scroll) {
140
+ windowSize = Math.max(MIN_BODY, windowSize - 1);
141
+ adjustScroll();
142
+ stickyHeader = `${import_picocolors.default.dim("│")} ${import_picocolors.default.underline(section.name)}`;
73
143
  }
74
- if (unchangedEntries.length > 0) {
75
- lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.underline("Unchanged")}`);
76
- for (const { item, idx } of unchangedEntries) {
77
- lines.push(formatRow(item, levels[idx], cursor === displayIdx));
78
- displayIdx++;
79
- }
80
- }
81
- lines.push(import_picocolors.default.dim("│"));
82
- const selectedCount = levels.filter((l) => l !== "skip").length;
83
- lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.dim(`${selectedCount} package${selectedCount !== 1 ? "s" : ""} selected`)}`);
84
- lines.push(`${import_picocolors.default.dim("")}`);
144
+ visibleRows = rows.slice(scroll, scroll + windowSize);
145
+ const above = scroll;
146
+ const below = rows.length - (scroll + windowSize);
147
+ if (above > 0) topIndicator = `${import_picocolors.default.dim("│")} ${import_picocolors.default.dim(`▲ ${above} more`)}`;
148
+ if (below > 0) bottomIndicator = `${import_picocolors.default.dim("│")} ${import_picocolors.default.dim(`▼ ${below} more`)}`;
149
+ }
150
+ lines.push(...headerChrome);
151
+ if (topIndicator !== null) lines.push(topIndicator);
152
+ if (stickyHeader !== null) lines.push(stickyHeader);
153
+ for (const row of visibleRows) if (row.kind === "separator") lines.push(import_picocolors.default.dim("│"));
154
+ else if (row.kind === "header") lines.push(`${import_picocolors.default.dim("")} ${import_picocolors.default.underline(row.text)}`);
155
+ else {
156
+ const item = items[row.itemIdx];
157
+ const isFocused = row.displayIdx === cursor;
158
+ lines.push(formatRow(item, levels[row.itemIdx], isFocused));
85
159
  }
160
+ if (bottomIndicator !== null) lines.push(bottomIndicator);
161
+ lines.push(...footerChrome);
86
162
  const output = lines.join("\n") + "\n";
87
163
  stdout.write(output);
88
164
  renderedLines = lines.length;
89
165
  }
166
+ function onResize() {
167
+ render();
168
+ }
90
169
  function cleanup() {
91
170
  stdin.removeListener("keypress", onKeypress);
171
+ stdout.removeListener("resize", onResize);
92
172
  rl.close();
93
173
  stdout.write("\x1B[?25h");
94
174
  if (stdin.isTTY) stdin.setRawMode(false);
@@ -141,8 +221,24 @@ async function bumpSelectPrompt(items) {
141
221
  render();
142
222
  }
143
223
  stdin.on("keypress", onKeypress);
224
+ stdout.on("resize", onResize);
144
225
  });
145
226
  }
227
+ /** Returns the section the focused item is in, plus the row index of its header. */
228
+ function getCurrentSection(cursor, changedCount, unchangedCount) {
229
+ if (cursor < changedCount) {
230
+ if (changedCount === 0) return null;
231
+ return {
232
+ headerRowIdx: 0,
233
+ name: "Changed"
234
+ };
235
+ }
236
+ if (unchangedCount === 0) return null;
237
+ return {
238
+ headerRowIdx: changedCount > 0 ? changedCount + 2 : 0,
239
+ name: "Unchanged"
240
+ };
241
+ }
146
242
  function formatRow(item, level, focused) {
147
243
  return `${import_picocolors.default.dim("│")} ${focused ? import_picocolors.default.cyan("›") : " "} ${focused ? import_picocolors.default.cyan(item.name) : item.name} ${import_picocolors.default.dim(`(${item.version})`)} ${formatLevel(level, focused)}`;
148
244
  }
@@ -4,7 +4,7 @@ import { a as DEP_TYPES } from "./types-Bkh-igOJ.mjs";
4
4
  import { a as loadConfig, o as loadPackageConfig, r as getBumpyDir } from "./config-D_4GYDJi.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
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";
7
+ import { a as getChangedFiles, r as getBaseCompareRef, s as getFileStatuses, u as readFileAtRef } from "./git-BWPimLgc.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) => {
@@ -4,9 +4,9 @@ import { n as detectPackageManager } from "./package-manager-Db_vTztt.mjs";
4
4
  import { i as recoverDeletedBumpFiles, r as readBumpFiles, s as discoverWorkspace, t as filterBranchBumpFiles } from "./bump-file-B7hmXZlB.mjs";
5
5
  import { a as DependencyGraph, t as assembleReleasePlan } from "./release-plan-mK7iGeGq.mjs";
6
6
  import { n as runArgs, r as runArgsAsync, s as tryRunArgs } from "./shell-C8KgKnMQ.mjs";
7
- import { f as withGitToken, i as getChangedFiles } from "./git-DJJ64SW9.mjs";
7
+ import { a as getChangedFiles, f as withGitToken } from "./git-BWPimLgc.mjs";
8
8
  import { t as randomName } from "./names-COooXAFg.mjs";
9
- import { n as findChangedPackages } from "./check-CsF0zh8r.mjs";
9
+ import { n as findChangedPackages } from "./check-Dvi0DIqC.mjs";
10
10
  import { t as resolveCommitMessage } from "./commit-message-CSWVKPJ-.mjs";
11
11
  import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { createHash } from "node:crypto";
@@ -157,7 +157,7 @@ async function ciPlanCommand(rootDir) {
157
157
  packageNames: plan.releases.map((r) => r.name)
158
158
  };
159
159
  else {
160
- const { findUnpublishedPackages } = await import("./publish-h6rM58Cq.mjs");
160
+ const { findUnpublishedPackages } = await import("./publish-VYBhDYFM.mjs");
161
161
  const unpublished = await findUnpublishedPackages(packages, config);
162
162
  if (unpublished.length > 0) output = {
163
163
  mode: "publish",
@@ -234,7 +234,7 @@ async function ciReleaseCommand(rootDir, opts) {
234
234
  const msg = bumpFiles.length === 0 ? "No pending bump files — checking for unpublished packages..." : "Bump files found but no packages would be released — checking for unpublished packages...";
235
235
  log.info(msg);
236
236
  const recoveredBumpFiles = recoverDeletedBumpFiles(rootDir);
237
- const { publishCommand } = await import("./publish-h6rM58Cq.mjs");
237
+ const { publishCommand } = await import("./publish-VYBhDYFM.mjs");
238
238
  await publishCommand(rootDir, {
239
239
  tag: opts.tag,
240
240
  recoveredBumpFiles
@@ -287,7 +287,7 @@ async function autoPublish(rootDir, config, plan, tag) {
287
287
  ], { cwd: rootDir });
288
288
  }
289
289
  log.step("Running bumpy publish...");
290
- const { publishCommand } = await import("./publish-h6rM58Cq.mjs");
290
+ const { publishCommand } = await import("./publish-VYBhDYFM.mjs");
291
291
  await publishCommand(rootDir, { tag });
292
292
  }
293
293
  /**
package/dist/cli.mjs CHANGED
@@ -31,7 +31,7 @@ async function main() {
31
31
  }
32
32
  case "add": {
33
33
  const rootDir = await findRoot();
34
- const { addCommand } = await import("./add-DQA1TVHA.mjs");
34
+ const { addCommand } = await import("./add-Dt1hddMt.mjs");
35
35
  await addCommand(rootDir, {
36
36
  packages: flags.packages,
37
37
  message: flags.message,
@@ -43,7 +43,7 @@ async function main() {
43
43
  }
44
44
  case "status": {
45
45
  const rootDir = await findRoot();
46
- const { statusCommand } = await import("./status-BbsDr6t7.mjs");
46
+ const { statusCommand } = await import("./status-DxzKPM8d.mjs");
47
47
  await statusCommand(rootDir, {
48
48
  json: flags.json === true,
49
49
  packagesOnly: flags.packages === true,
@@ -61,7 +61,7 @@ async function main() {
61
61
  }
62
62
  case "generate": {
63
63
  const rootDir = await findRoot();
64
- const { generateCommand } = await import("./generate-CvCvUaRV.mjs");
64
+ const { generateCommand } = await import("./generate-DohUlhu3.mjs");
65
65
  await generateCommand(rootDir, {
66
66
  from: flags.from,
67
67
  dryRun: flags["dry-run"] === true,
@@ -71,7 +71,7 @@ async function main() {
71
71
  }
72
72
  case "check": {
73
73
  const rootDir = await findRoot();
74
- const { checkCommand } = await import("./check-CsF0zh8r.mjs").then((n) => n.t);
74
+ const { checkCommand } = await import("./check-Dvi0DIqC.mjs").then((n) => n.t);
75
75
  const hookValue = flags.hook;
76
76
  if (hookValue && hookValue !== "pre-commit" && hookValue !== "pre-push") {
77
77
  log.error(`Invalid --hook value "${hookValue}". Expected "pre-commit" or "pre-push".`);
@@ -89,17 +89,17 @@ async function main() {
89
89
  const subcommand = args[1];
90
90
  const ciFlags = parseFlags(args.slice(2));
91
91
  if (subcommand === "check") {
92
- const { ciCheckCommand } = await import("./ci-CIamssoq.mjs");
92
+ const { ciCheckCommand } = await import("./ci-B7gF6CFP.mjs");
93
93
  await ciCheckCommand(rootDir, {
94
94
  comment: ciFlags.comment !== void 0 ? ciFlags.comment === true : void 0,
95
95
  strict: ciFlags.strict === true,
96
96
  noFail: ciFlags["no-fail"] === true
97
97
  });
98
98
  } else if (subcommand === "plan") {
99
- const { ciPlanCommand } = await import("./ci-CIamssoq.mjs");
99
+ const { ciPlanCommand } = await import("./ci-B7gF6CFP.mjs");
100
100
  await ciPlanCommand(rootDir);
101
101
  } else if (subcommand === "release") {
102
- const { ciReleaseCommand } = await import("./ci-CIamssoq.mjs");
102
+ const { ciReleaseCommand } = await import("./ci-B7gF6CFP.mjs");
103
103
  const expectModeFlag = ciFlags["expect-mode"];
104
104
  const autoPublishFlag = ciFlags["auto-publish"] === true;
105
105
  if (expectModeFlag !== void 0 && expectModeFlag !== "version-pr" && expectModeFlag !== "publish") {
@@ -127,7 +127,7 @@ async function main() {
127
127
  }
128
128
  case "publish": {
129
129
  const rootDir = await findRoot();
130
- const { publishCommand } = await import("./publish-h6rM58Cq.mjs");
130
+ const { publishCommand } = await import("./publish-VYBhDYFM.mjs");
131
131
  await publishCommand(rootDir, {
132
132
  dryRun: flags["dry-run"] === true,
133
133
  tag: flags.tag,
@@ -151,7 +151,7 @@ async function main() {
151
151
  }
152
152
  case "--version":
153
153
  case "-v":
154
- console.log(`bumpy 1.13.0`);
154
+ console.log(`bumpy 1.13.2`);
155
155
  break;
156
156
  case "help":
157
157
  case "--help":
@@ -171,7 +171,7 @@ async function main() {
171
171
  }
172
172
  function printHelp() {
173
173
  console.log(`
174
- ${colorize(`🐸 bumpy v1.13.0`, "bold")} - Modern monorepo versioning
174
+ ${colorize(`🐸 bumpy v1.13.2`, "bold")} - Modern monorepo versioning
175
175
 
176
176
  Usage: bumpy <command> [options]
177
177
 
@@ -3,7 +3,7 @@ import { t as ensureDir } from "./fs-CBXKZhoU.mjs";
3
3
  import { a as loadConfig, r as getBumpyDir } from "./config-D_4GYDJi.mjs";
4
4
  import { a as writeBumpFile, o as discoverPackages } from "./bump-file-B7hmXZlB.mjs";
5
5
  import { s as tryRunArgs } from "./shell-C8KgKnMQ.mjs";
6
- import { r as getBranchCommits, s as getFilesChangedInCommit } from "./git-DJJ64SW9.mjs";
6
+ import { c as getFilesChangedInCommit, i as getBranchCommits } from "./git-BWPimLgc.mjs";
7
7
  import { n as slugify, t as randomName } from "./names-COooXAFg.mjs";
8
8
  import { relative } from "node:path";
9
9
  //#region src/commands/generate.ts
@@ -8,13 +8,23 @@ function createTag(tag, opts) {
8
8
  tag
9
9
  ], opts);
10
10
  }
11
- /** Push tags to remote, using BUMPY_GH_TOKEN if available */
12
- function pushWithTags(opts) {
11
+ /**
12
+ * Force-push a single tag to origin, using BUMPY_GH_TOKEN if available.
13
+ *
14
+ * Force is required because `gh release create --draft --target SHA` creates
15
+ * the tag on the remote at draft-creation time. If a previous publish attempt
16
+ * failed and HEAD has since moved, the remote tag points at the stale SHA —
17
+ * `git push --tags` would reject. The caller is responsible for ensuring the
18
+ * local tag is at the correct SHA (i.e. only call after a successful publish).
19
+ */
20
+ function forcePushTag(tag, opts) {
13
21
  withGitToken(opts?.cwd, () => {
14
22
  runArgs([
15
23
  "git",
16
24
  "push",
17
- "--tags"
25
+ "origin",
26
+ `refs/tags/${tag}`,
27
+ "--force"
18
28
  ], opts);
19
29
  });
20
30
  }
@@ -260,4 +270,4 @@ function getFileStatuses(dir, opts) {
260
270
  return statuses;
261
271
  }
262
272
  //#endregion
263
- export { getCurrentBranch as a, hasUncommittedChanges as c, tagExists as d, withGitToken as f, getChangedFiles as i, pushWithTags as l, getBaseCompareRef as n, getFileStatuses as o, getBranchCommits as r, getFilesChangedInCommit as s, createTag as t, readFileAtRef as u };
273
+ export { getChangedFiles as a, getFilesChangedInCommit as c, tagExists as d, withGitToken as f, getBranchCommits as i, hasUncommittedChanges as l, forcePushTag as n, getCurrentBranch as o, getBaseCompareRef as r, getFileStatuses as s, createTag as t, readFileAtRef as u };
package/dist/index.mjs CHANGED
@@ -4,5 +4,5 @@ import { a as writeBumpFile, n as parseBumpFile, o as discoverPackages, r as rea
4
4
  import { a as DependencyGraph, i as stripProtocol, n as bumpVersion, r as satisfies, t as assembleReleasePlan } from "./release-plan-mK7iGeGq.mjs";
5
5
  import { a as prependToChangelog, i as loadFormatter, n as generateChangelogEntry, t as defaultFormatter } from "./changelog-CbaET5V6.mjs";
6
6
  import { t as applyReleasePlan } from "./apply-release-plan-DD2R7SL2.mjs";
7
- import { t as publishPackages } from "./publish-pipeline-DSj14dW6.mjs";
7
+ import { t as publishPackages } from "./publish-pipeline-BPedWvKS.mjs";
8
8
  export { BUMP_LEVELS, DEFAULT_BUMP_RULES, DEFAULT_CONFIG, DEFAULT_PUBLISH_CONFIG, DEP_TYPES, DependencyGraph, applyReleasePlan, assembleReleasePlan, bumpLevel, bumpVersion, defaultFormatter, discoverPackages, findRoot, generateChangelogEntry, getBumpyDir, hasCascade, loadConfig, loadFormatter, matchGlob, maxBump, normalizeCascadeConfig, parseBumpFile, prependToChangelog, publishPackages, readBumpFiles, satisfies, stripProtocol, writeBumpFile };
@@ -5,9 +5,9 @@ import { s as discoverWorkspace } from "./bump-file-B7hmXZlB.mjs";
5
5
  import { a as DependencyGraph } from "./release-plan-mK7iGeGq.mjs";
6
6
  import { r as runArgsAsync, s as tryRunArgs } from "./shell-C8KgKnMQ.mjs";
7
7
  import { i as loadFormatter, n as generateChangelogEntry } from "./changelog-CbaET5V6.mjs";
8
- import { c as hasUncommittedChanges, l as pushWithTags } from "./git-DJJ64SW9.mjs";
9
- import { t as publishPackages } from "./publish-pipeline-DSj14dW6.mjs";
10
- import { CI_PLAN_CACHE_PATH } from "./ci-CIamssoq.mjs";
8
+ import { d as tagExists, l as hasUncommittedChanges, n as forcePushTag } from "./git-BWPimLgc.mjs";
9
+ import { n as willUseOidcExclusively, t as publishPackages } from "./publish-pipeline-BPedWvKS.mjs";
10
+ import { CI_PLAN_CACHE_PATH } from "./ci-B7gF6CFP.mjs";
11
11
  //#region src/core/github-release.ts
12
12
  /** Get the current HEAD commit SHA */
13
13
  function getHeadSha(rootDir) {
@@ -332,6 +332,17 @@ async function publishCommand(rootDir, opts) {
332
332
  else log.bold("Publishing:");
333
333
  for (const r of toPublish) console.log(` ${r.name}@${colorize(r.newVersion, "cyan")}`);
334
334
  console.log();
335
+ if (willUseOidcExclusively(rootDir)) {
336
+ const newPackages = await findPackagesMissingFromNpm(toPublish, packages);
337
+ if (newPackages.length > 0) {
338
+ const logFn = opts.dryRun ? log.warn : log.error;
339
+ logFn(`Trusted publishing (OIDC) cannot create a new package. The following don't exist on npm yet:`);
340
+ for (const name of newPackages) logFn(` • ${name}`);
341
+ logFn(`Publish a 0.0.0 placeholder version manually to claim the name, then configure`);
342
+ logFn(`trusted publishing on npmjs.com. Bumpy will then publish the real version via OIDC.`);
343
+ if (!opts.dryRun) process.exit(1);
344
+ }
345
+ }
335
346
  const formatter = config.changelog !== false ? await loadFormatter(config.changelog, rootDir) : void 0;
336
347
  const ghAvailable = isGhAvailable();
337
348
  const publishTargetsByPkg = /* @__PURE__ */ new Map();
@@ -489,12 +500,22 @@ async function publishCommand(rootDir, opts) {
489
500
  log.error(`Failed ${result.failed.length}: ${result.failed.map((f) => `${f.name} (${f.error})`).join(", ")}`);
490
501
  process.exit(1);
491
502
  }
492
- if (!opts.dryRun && !opts.noPush && result.published.length > 0) try {
503
+ if (!opts.dryRun && !opts.noPush && result.published.length > 0) {
504
+ const failed = new Set(result.failed.map((f) => f.name));
505
+ const pushed = [];
493
506
  log.step("Pushing tags...");
494
- pushWithTags({ cwd: rootDir });
495
- log.success("Pushed tags to remote");
496
- } catch (err) {
497
- log.warn(`Failed to push tags: ${err instanceof Error ? err.message : err}`);
507
+ for (const release of releasePlan.releases) {
508
+ if (failed.has(release.name)) continue;
509
+ const tag = `${release.name}@${release.newVersion}`;
510
+ if (!tagExists(tag, { cwd: rootDir })) continue;
511
+ try {
512
+ forcePushTag(tag, { cwd: rootDir });
513
+ pushed.push(tag);
514
+ } catch (err) {
515
+ log.warn(` Failed to push tag ${tag}: ${err instanceof Error ? err.message : err}`);
516
+ }
517
+ }
518
+ if (pushed.length > 0) log.success(`Pushed ${pushed.length} tag(s) to remote`);
498
519
  }
499
520
  if (!ghAvailable && result.published.length > 0) await createIndividualReleases(releasePlan.releases.filter((r) => result.published.some((p) => p.name === r.name)), releasePlan.bumpFiles, rootDir, {
500
521
  dryRun: opts.dryRun,
@@ -618,5 +639,38 @@ async function checkIfPublished(name, version, pkgConfig) {
618
639
  return false;
619
640
  }
620
641
  }
642
+ /**
643
+ * Check whether a package exists on npm at all (any version).
644
+ * Returns true if the package is registered, false if it doesn't exist or the query fails.
645
+ */
646
+ async function packageExistsOnNpm(name, registry) {
647
+ const args = [
648
+ "npm",
649
+ "info",
650
+ name,
651
+ "name"
652
+ ];
653
+ if (registry) args.push("--registry", registry);
654
+ try {
655
+ return (await runArgsAsync(args)).trim() === name;
656
+ } catch {
657
+ return false;
658
+ }
659
+ }
660
+ /**
661
+ * Filter `toPublish` to package names that don't exist on npm yet.
662
+ * Skips packages not going through the standard npm publish flow.
663
+ */
664
+ async function findPackagesMissingFromNpm(toPublish, packages) {
665
+ const missing = [];
666
+ await Promise.all(toPublish.map(async (release) => {
667
+ const pkg = packages.get(release.name);
668
+ const pkgConfig = pkg.bumpy || {};
669
+ if (pkgConfig.publishCommand || pkgConfig.skipNpmPublish) return;
670
+ if (pkg.private && !pkgConfig.publishCommand) return;
671
+ if (!await packageExistsOnNpm(release.name, pkgConfig.registry)) missing.push(release.name);
672
+ }));
673
+ return missing;
674
+ }
621
675
  //#endregion
622
676
  export { findUnpublishedPackages, publishCommand };
@@ -3,7 +3,7 @@ import { a as readJson, u as updateJsonNestedField } from "./fs-CBXKZhoU.mjs";
3
3
  import { s as resolveCatalogDep } from "./package-manager-Db_vTztt.mjs";
4
4
  import { i as stripProtocol } from "./release-plan-mK7iGeGq.mjs";
5
5
  import { i as runAsync, o as sq, r as runArgsAsync, s as tryRunArgs } from "./shell-C8KgKnMQ.mjs";
6
- import { d as tagExists, t as createTag } from "./git-DJJ64SW9.mjs";
6
+ import { d as tagExists, t as createTag } from "./git-BWPimLgc.mjs";
7
7
  import { resolve } from "node:path";
8
8
  import { unlink } from "node:fs/promises";
9
9
  import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
@@ -23,6 +23,21 @@ function detectOidcProvider() {
23
23
  if (process.env.CIRCLECI && process.env.NPM_ID_TOKEN) return "circleci";
24
24
  return null;
25
25
  }
26
+ /**
27
+ * Returns true when OIDC trusted publishing is the only available npm auth path:
28
+ * an OIDC provider is detected AND no token env vars or .npmrc auth are present.
29
+ *
30
+ * Used to gate checks that only matter when OIDC will definitely be used — e.g.
31
+ * erroring when a brand-new package can't be bootstrapped via trusted publishing.
32
+ * Detection alone is leaky (id-token: write is also set for provenance), so this
33
+ * helper avoids false positives when a token fallback exists.
34
+ */
35
+ function willUseOidcExclusively(rootDir) {
36
+ if (!detectOidcProvider()) return false;
37
+ if (process.env.NPM_TOKEN || process.env.NODE_AUTH_TOKEN) return false;
38
+ const npmrcPath = resolve(rootDir, ".npmrc");
39
+ return !(existsSync(npmrcPath) ? readFileSync(npmrcPath, "utf-8") : "").includes(":_authToken=");
40
+ }
26
41
  const OIDC_NPM_UPGRADE_HINTS = {
27
42
  "github-actions": "Add `actions/setup-node@v6` with `node-version: lts/*` to your workflow",
28
43
  gitlab: "Use a Node.js image with npm >= 11.5.1 or run `npm install -g npm@latest`",
@@ -306,4 +321,4 @@ async function resolveProtocolsInPlace(pkg, packages, releasePlan, catalogs) {
306
321
  }
307
322
  }
308
323
  //#endregion
309
- export { publishPackages as t };
324
+ export { willUseOidcExclusively as n, publishPackages as t };
@@ -2,7 +2,7 @@ import { n as log, t as colorize } from "./logger-BgksGFuf.mjs";
2
2
  import { a as loadConfig } from "./config-D_4GYDJi.mjs";
3
3
  import { o as discoverPackages, r as readBumpFiles, t as filterBranchBumpFiles } from "./bump-file-B7hmXZlB.mjs";
4
4
  import { a as DependencyGraph, t as assembleReleasePlan } from "./release-plan-mK7iGeGq.mjs";
5
- import { a as getCurrentBranch, i as getChangedFiles } from "./git-DJJ64SW9.mjs";
5
+ import { a as getChangedFiles, o as getCurrentBranch } from "./git-BWPimLgc.mjs";
6
6
  //#region src/commands/status.ts
7
7
  async function statusCommand(rootDir, opts) {
8
8
  const config = await loadConfig(rootDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@varlock/bumpy",
3
- "version": "1.13.0",
3
+ "version": "1.13.2",
4
4
  "description": "Modern monorepo versioning and changelog tool",
5
5
  "keywords": [
6
6
  "bump",