<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Niccolò Olivieri Achille</title>
    <description>The latest articles on Forem by Niccolò Olivieri Achille (@zweer).</description>
    <link>https://forem.com/zweer</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F285321%2Fe016f6bd-114f-4b3b-b5f4-7d5eb8ad80f8.jpeg</url>
      <title>Forem: Niccolò Olivieri Achille</title>
      <link>https://forem.com/zweer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/zweer"/>
    <language>en</language>
    <item>
      <title>Why I built yet another release tool for npm</title>
      <dc:creator>Niccolò Olivieri Achille</dc:creator>
      <pubDate>Fri, 13 Feb 2026 22:33:56 +0000</pubDate>
      <link>https://forem.com/zweer/why-i-built-yet-another-release-tool-for-npm-3m4m</link>
      <guid>https://forem.com/zweer/why-i-built-yet-another-release-tool-for-npm-3m4m</guid>
      <description>&lt;p&gt;It was a Tuesday night. I was staring at a CI pipeline that had just published three packages to npm — packages that hadn't changed. Not a single line of code was different, but lerna decided they needed a new version anyway. Again.&lt;/p&gt;

&lt;p&gt;I deleted the tags. Unpublished the versions. Pushed a fix. Went to bed angry.&lt;/p&gt;

&lt;p&gt;This wasn't the first time. And it wasn't the last.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Long Road
&lt;/h2&gt;

&lt;p&gt;I've been trying to solve monorepo releases for years. At work, on personal projects — the problem is always the same: you have multiple packages in one repo, and you need to release them independently, with proper changelogs, proper tags, and proper npm publishes.&lt;/p&gt;

&lt;p&gt;It sounds simple. It's not.&lt;/p&gt;

&lt;h3&gt;
  
  
  auto + lerna (the first attempt)
&lt;/h3&gt;

&lt;p&gt;At work, about two years ago, I introduced &lt;a href="https://github.com/intuit/auto" rel="noopener noreferrer"&gt;auto&lt;/a&gt; by Intuit. It looked perfect: label-based releases, GitHub integration, monorepo support via lerna.&lt;/p&gt;

&lt;p&gt;The honeymoon lasted about a week.&lt;/p&gt;

&lt;p&gt;Lerna had a bizarre dependency conflict with conventional commits. You had to pin a specific lerna version, otherwise the conventional commits parser wouldn't even load. Update lerna? Conventional commits break. Update conventional commits? Lerna breaks. It was a game you couldn't win.&lt;/p&gt;

&lt;p&gt;Then there was the phantom release problem. &lt;code&gt;auto release&lt;/code&gt; would sometimes &lt;a href="https://github.com/intuit/auto/issues/596" rel="noopener noreferrer"&gt;create wrong tags&lt;/a&gt; in lerna monorepos. And lerna itself would &lt;a href="https://github.com/lerna/lerna/issues/1357" rel="noopener noreferrer"&gt;publish packages that hadn't changed&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I liked auto enough to contribute — I opened a couple of PRs that got merged. But the lerna dependency was a dead weight. Lerna had been abandoned by its original maintainer, and even after the Nrwl/Nx team adopted it, the fundamental issues remained.&lt;/p&gt;

&lt;h3&gt;
  
  
  semantic-release (the popular choice)
&lt;/h3&gt;

&lt;p&gt;Everyone recommends semantic-release. It's the most popular release tool in the npm ecosystem. So I tried it.&lt;/p&gt;

&lt;p&gt;One problem: &lt;strong&gt;no monorepo support&lt;/strong&gt;. At all. It assumes one repo = one package.&lt;/p&gt;

&lt;p&gt;The community has tried to fix this. There's &lt;code&gt;semantic-release-monorepo&lt;/code&gt;, &lt;code&gt;multi-semantic-release&lt;/code&gt;, and various forks. But &lt;code&gt;multi-semantic-release&lt;/code&gt; literally describes itself as a "proof of concept" and warns it "may not be fundamentally stable enough for important production use." Not exactly confidence-inspiring.&lt;/p&gt;

&lt;h3&gt;
  
  
  release-it (the interactive one)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/release-it/release-it" rel="noopener noreferrer"&gt;release-it&lt;/a&gt; is great for single packages. Interactive prompts, clean workflow, good plugin system.&lt;/p&gt;

&lt;p&gt;But monorepo? The maintainer's response in &lt;a href="https://github.com/release-it/release-it/issues/831" rel="noopener noreferrer"&gt;issue #831&lt;/a&gt;: "There is no built-in support for monorepos in release-it." The issue was closed. Multiple follow-up requests (#858, #516) — same answer.&lt;/p&gt;

&lt;p&gt;And it had the same phantom release problem: &lt;a href="https://github.com/release-it/release-it/issues/683" rel="noopener noreferrer"&gt;empty releases published&lt;/a&gt; when nothing had changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  release-please (the Google one)
&lt;/h3&gt;

&lt;p&gt;Google's &lt;a href="https://github.com/googleapis/release-please" rel="noopener noreferrer"&gt;release-please&lt;/a&gt; seemed promising. Conventional commits, monorepo support, PR-based workflow.&lt;/p&gt;

&lt;p&gt;Then I tried the GitHub Action.&lt;/p&gt;

&lt;p&gt;The original action (&lt;code&gt;google-github-actions/release-please-action&lt;/code&gt;) was &lt;a href="https://github.com/google-github-actions/release-please-action" rel="noopener noreferrer"&gt;archived in August 2024&lt;/a&gt;. Migrated to a new org with breaking changes. The v3 to v4 upgrade was described as a &lt;a href="https://danwakeem.medium.com/beware-the-release-please-v4-github-action-ee71ff9de151" rel="noopener noreferrer"&gt;"nasty surprise"&lt;/a&gt; by the community. PRs would &lt;a href="https://github.com/googleapis/release-please/issues/1946" rel="noopener noreferrer"&gt;randomly stop being created&lt;/a&gt;. 197 open issues and counting.&lt;/p&gt;

&lt;p&gt;The tool itself is powerful, but the GitHub Action — the thing most people actually use — was a minefield.&lt;/p&gt;

&lt;h3&gt;
  
  
  changesets (the last hope)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/changesets/changesets" rel="noopener noreferrer"&gt;changesets&lt;/a&gt; is probably the most widely used monorepo release tool. Used by Vercel, Radix, and many others.&lt;/p&gt;

&lt;p&gt;I tried it. And I understood why people use it. It's solid.&lt;/p&gt;

&lt;p&gt;But it requires you to abandon conventional commits. Instead, you create a &lt;code&gt;.changeset/something.md&lt;/code&gt; file for every change. Every PR needs a changeset file. Every single one.&lt;/p&gt;

&lt;p&gt;For a team that already uses conventional commits — that already writes &lt;code&gt;feat:&lt;/code&gt; and &lt;code&gt;fix:&lt;/code&gt; in every commit message — this felt like a step backward. I was adding process, not removing it.&lt;/p&gt;

&lt;p&gt;And the PR workflow is mandatory. There's no "just release now" option.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Wanted
&lt;/h2&gt;

&lt;p&gt;After years of trying every tool, I knew exactly what I needed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Monorepo-native&lt;/strong&gt; — not bolted on, not a plugin, not a wrapper. Built for monorepos from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conventional commits&lt;/strong&gt; — I already write them. Use them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero config&lt;/strong&gt; — it should work out of the box for the 90% case (npm + GitHub).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexible workflow&lt;/strong&gt; — sometimes I want to release now. Sometimes I want a PR. Let me choose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin system&lt;/strong&gt; — but a real one, not "write a shell script and hope for the best."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No phantom releases&lt;/strong&gt; — if nothing changed, do nothing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No existing tool checked all six boxes.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I Built bonvoy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Zweer/bonvoy" rel="noopener noreferrer"&gt;bonvoy&lt;/a&gt; ("bon voyage to your releases!") is a plugin-based release tool for npm packages and monorepos.&lt;/p&gt;

&lt;p&gt;The core is intentionally tiny: a hook system (powered by &lt;a href="https://github.com/webpack/tapable" rel="noopener noreferrer"&gt;tapable&lt;/a&gt;, the same library webpack uses), workspace detection, and config loading. Everything else is a plugin.&lt;/p&gt;

&lt;p&gt;Five default plugins handle the common case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;conventional&lt;/strong&gt; — parse commits for version bumps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;changelog&lt;/strong&gt; — generate CHANGELOG.md&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;git&lt;/strong&gt; — commit, tag, push&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt; — publish with OIDC provenance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;github&lt;/strong&gt; — create GitHub releases&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What it looks like
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; bonvoy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx bonvoy shipit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. bonvoy reads your git history, figures out which packages changed, calculates version bumps from conventional commits, generates changelogs, publishes to npm, and creates GitHub releases.&lt;/p&gt;

&lt;p&gt;Want to preview first?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx bonvoy shipit &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Force a specific version?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx bonvoy shipit 2.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only release one package?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx bonvoy shipit &lt;span class="nt"&gt;--package&lt;/span&gt; @scope/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want a PR workflow instead?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx bonvoy prepare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  No config needed
&lt;/h3&gt;

&lt;p&gt;For the common case (npm monorepo + GitHub), you don't need a config file. bonvoy detects your workspaces, reads your conventional commits, and does the right thing.&lt;/p&gt;

&lt;p&gt;When you need to customize:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// bonvoy.config.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;versioning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;independent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tagFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{name}@{version}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;changelog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;feat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;✨ Features&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🐛 Bug Fixes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GitHub Actions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Release&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;bump&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;bump&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(patch/minor/major/x.y.z)'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
          &lt;span class="na"&gt;registry-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://registry.npmjs.org'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx bonvoy shipit ${{ github.event.inputs.bump }}&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Is It For You?
&lt;/h2&gt;

&lt;p&gt;bonvoy is a good fit if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a monorepo with npm workspaces&lt;/li&gt;
&lt;li&gt;You use (or want to use) conventional commits&lt;/li&gt;
&lt;li&gt;You want releases to be simple and predictable&lt;/li&gt;
&lt;li&gt;You want to extend behavior with plugins&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's probably &lt;strong&gt;not&lt;/strong&gt; for you if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You use pnpm or yarn PnP workspaces (npm workspaces only, for now)&lt;/li&gt;
&lt;li&gt;You need a battle-tested enterprise solution (semantic-release has years of production use)&lt;/li&gt;
&lt;li&gt;Your team is already happy with changesets&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; bonvoy
npx bonvoy shipit &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;📚 &lt;a href="https://zweer.github.io/bonvoy" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 &lt;a href="https://github.com/Zweer/bonvoy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;a href="https://www.npmjs.com/org/bonvoy" rel="noopener noreferrer"&gt;npm&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever been frustrated by monorepo releases, I'd love to hear your story. And if you try bonvoy, let me know what breaks — I'm using it to release itself, so I'm eating my own dog food.&lt;/p&gt;

&lt;p&gt;Bon voyage! 🚢&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>npm</category>
      <category>opensource</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
