<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Yein Sung</title><description>Developer blog — software engineering, open-source tools, and development practices.</description><link>https://blog.castle-yein.com/</link><item><title>Your Open-Source CLI Deserves Better Bug Reports</title><link>https://blog.castle-yein.com/open-source-cli-better-bug-reports/</link><guid isPermaLink="true">https://blog.castle-yein.com/open-source-cli-better-bug-reports/</guid><description>Up to 77% of OSS bug reports end up invalid. AI agents now flood security programs with hundreds at once. Why existing tools don&apos;t fix this, and what does.</description><pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate><content:encoded>You get a GitHub notification. Someone filed an issue titled &quot;it doesn&apos;t work.&quot; No version. No OS. No stack trace. Just that.

You reply asking for their environment details and a reproduction case. Silence. Three weeks later, the stale bot closes it. Maybe it was a real bug. Maybe it was a misconfigured PATH. You&apos;ll never know.

That pattern is happening thousands of times a day across open-source projects. And it&apos;s getting worse.

---

## The Bug Report Quality Crisis

Open-source bug reports have always had a signal-to-noise problem. A [study of Mozilla and Firefox bugs](https://www.researchgate.net/publication/220720098_Why_are_Bug_Reports_Invalid) found that between 31% and 77% of reports were classified as invalid or non-reproducible, depending on the project. Duplicate reports add to the pile: depending on the project, [somewhere between 10% and 30% of submitted reports turn out to be duplicates](https://ieeexplore.ieee.org/document/8595210) of existing issues.

That&apos;s a lot of triage for reports that go nowhere.

Now add AI-generated reports to the mix. The problem is showing up most acutely in security disclosure and bug bounty programs, where [autonomous agents are flooding projects with hundreds of reports at a time](https://www.axios.com/2026/03/10/ai-agents-spam-the-volunteers-securing-open-source-software). But the underlying dynamic, low-signal reports overwhelming volunteer maintainers, applies to everyday bug reports too.

Curl is the clearest case study. Daniel Stenberg, the project&apos;s creator and maintainer, documented the slide in his [January 2026 post](https://daniel.haxx.se/blog/2026/01/26/the-end-of-the-curl-bug-bounty/): curl&apos;s HackerOne program historically confirmed somewhere north of 15% of security reports. By 2025, that rate had dropped below 5%. Fewer than one in twenty reports was real. In Stenberg&apos;s words, the project saw &quot;an explosion in AI slop reports combined with a lower quality even in the reports that were not obvious slop.&quot; He ended the bounty program&apos;s monetary rewards in January 2026 after nearly seven years and over $100,000 paid out. Curl [later reopened HackerOne for security reports without bounties](https://daniel.haxx.se/blog/2026/02/27/security-reports-are-back/), but the signal-to-noise damage was done. The [Register covered the original announcement](https://www.theregister.com/2026/01/21/curl_ends_bug_bounty/).

Curl is a 28-year-old project with a known and active maintainer. Imagine the situation for a two-year-old CLI tool with a single unpaid developer.

The [Tidelift maintainer survey](https://www.sonarsource.com/blog/maintainer-burnout-is-real/) put numbers on what this costs: 58% of maintainers have quit or considered quitting. 44% cite burnout specifically. And a large share of that burnout comes from the time spent not writing code, but triaging low-quality issues.

---

## Why Existing Solutions Don&apos;t Fix This

The standard toolkit for managing bug reports is: issue templates, stale bots, and sometimes error monitoring services. None of these address the root problem.

### Issue templates

Nearly all large-scale open-source projects use GitHub issue templates. Templates improve compliance: they can require reproduction links, filter incomplete reports, and auto-close issues that don&apos;t meet minimum criteria. But they still rely on user effort, and they can&apos;t capture runtime context automatically.

Shelley Vohr, a Node.js and Electron maintainer, [describes the result directly](https://dev.to/codebytere/open-source-the-art-of-the-issue-pnd): &quot;With no actionable steps, I&apos;m forced to make educated guesses, which may harm my ability to understand the underlying causal pathways of the bug.&quot; She also notes: &quot;Neglecting to fill out an issue template both ruffles maintainer feathers and results in them needing to ask you for more information - delaying any potential fix.&quot;

The structural problem with templates is that they depend entirely on user motivation. When someone&apos;s CLI tool is broken and they&apos;re frustrated, filling out a structured form carefully is not their top priority.

### Stale bots

Most large open-source projects have adopted a stale bot. A stale bot closes issues that haven&apos;t been updated in a set period, usually 60-90 days.

This is symptom treatment. Stale bots reduce the open issue count, but they don&apos;t fix the underlying failure: the issue was filed without enough information, the maintainer asked for more, and the reporter never replied. The bug may still exist. The stale bot just makes it invisible.

### Error monitoring services (Sentry, Bugsnag)

These tools are designed for web application operators, not for CLI maintainers distributing a tool to thousands of independent users.

The concern runs deeper than pricing. When you ship a web app, you control the deployment environment and can establish a privacy policy. When you ship a CLI tool, you&apos;re asking an SDK to capture data on someone else&apos;s machine. What gets collected, where it goes, and whether the user knows about it are all questions your project has to answer. [Sentry&apos;s open-source program](https://sentry.io/for/open-source/) exists, but the privacy and control questions remain the same regardless of cost.

Then there&apos;s infrastructure. Self-hosted Sentry [requires 4 CPU cores, 16 GB RAM plus 16 GB swap, and 20 GB disk](https://develop.sentry.dev/self-hosted/) at minimum. That&apos;s not a realistic ask for a solo maintainer of a 2,000-star CLI tool.

### Telemetry

The obvious alternative is opt-in telemetry. But telemetry [can trigger strong trust and consent concerns](https://posthog.com/blog/open-source-telemetry-ethical) in open-source communities. Homebrew&apos;s [GitHub issue #142](https://github.com/Homebrew/brew/issues/142) became an extended community fight when data was sent without explicit permission. Next.js faced similar backlash. The trust cost of adding telemetry to an open-source CLI is real, and for many projects, it&apos;s not worth paying.

Russ Cox [proposed a transparent telemetry model for Go](https://research.swtch.com/telemetry-intro) in 2023, where data is collected openly and designed to be non-identifying. It&apos;s thoughtful work. It also requires infrastructure, community buy-in, and a level of organizational trust that the Go team has earned over 15 years. Most CLI projects don&apos;t have that runway.

---

## There&apos;s a Third Option

The current framing treats this as a binary choice: either you use Sentry-style automatic collection (privacy risk, infrastructure burden, community backlash) or you rely on manual bug reports (the broken status quo).

That framing is wrong. It&apos;s missing a third option.

The missing path: capture error data locally, sanitize it on-device before the user sees it, let the user review exactly what will be submitted, and then publish a structured report with one confirmation step.

No server. No data leaving the user&apos;s machine without review. No infrastructure to maintain. No trust problem.

I&apos;ve been thinking about this as a tool-building problem, not just a maintainer etiquette problem. If the tooling captures and structures the information at error time, you don&apos;t need to convince users to do it manually.

This is the approach [Cluvo](https://github.com/syi0808/cluvo) takes.

---

## How Cluvo Works

Cluvo is an SDK that wraps your CLI&apos;s error handling and turns crashes into structured GitHub issues with user consent at every step.

The pipeline is:

1. **Collector**: catches the error and gathers context (error message, stack trace, OS, runtime version, architecture, command args, git SHA)
2. **Sanitizer**: strips credentials, tokens, and private paths before anything is shown to the user
3. **Store**: saves the sanitized report locally
4. **Matcher**: searches your project&apos;s existing GitHub issues to detect duplicates before submission
5. **Presenter**: shows the user a preview of exactly what will be submitted, in the terminal
6. **Publisher**: opens a pre-filled GitHub issue via browser, `gh` CLI, GitHub API, or saves to a local file as fallback

The integration takes a few lines:

```typescript
import { Reporter } from &quot;@cluvo/sdk&quot;;

const reporter = new Reporter({
  repo: &quot;your-org/your-cli&quot;,
  app: { name: &quot;your-cli&quot;, version: &quot;1.0.0&quot; },
});

await reporter.wrapCommand(async () =&gt; {
  await runCLI();
});
```

From the user&apos;s perspective, when something breaks, they see a terminal prompt:

```
✗ Command failed: parse error in config.yaml

A bug report has been prepared. Here&apos;s what it contains:
  • OS: macOS 14.3.1 (arm64)
  • Node: v22.14.0
  • Command: your-cli build --watch
  • Error: ParseError: unexpected token at line 23

[Submit to GitHub] [View full report] [Cancel]
```

The user controls submission. Nothing is sent silently. And the report that lands in your GitHub issues includes environment, command, and stack trace by default.

### Core design principles

**Zero-server architecture.** Cluvo has no backend. Reports are published directly to GitHub via the user&apos;s own auth (browser session or `gh` CLI token). There&apos;s no Cluvo server that sees or stores anything.

**Privacy-safe by default.** The sanitizer runs before the preview. Users see the sanitized version, not the raw error. Common patterns (API keys, tokens, `~/.ssh/` paths, AWS credential patterns) are stripped automatically. You can configure additional sanitization rules.

**Consent-first.** The user sees a preview and explicitly confirms before anything leaves their machine. Opt-out is always one keypress away.

**Fallback chain.** If the browser can&apos;t be opened, Cluvo tries the `gh` CLI. If that&apos;s not available, it falls back to the GitHub API. If that fails, it saves a markdown file locally with instructions for manual filing.

---

## What This Changes for Maintainers

### Friction asymmetry

The reason bug reports are low quality isn&apos;t that users are careless. It&apos;s that the cost of filing a good report falls entirely on the user, while the benefit (a fixed bug) may not materialize for months. Cluvo shifts this: the user&apos;s burden is one keypress to confirm a pre-filled report. The motivation to get the bug fixed is often enough to clear that bar.

### Structured reports by default

Because the SDK collects environment data automatically, submitted reports include OS, runtime version, command invocation, and stack trace without the user having to type any of it. You stop asking &quot;what version are you on?&quot; in every issue.

### Duplicate detection before submission

The Matcher step runs against your existing GitHub issues before showing the user the submission prompt. If a match is found, the user is told &quot;this looks like it might be related to issue #47&quot; with a link. Duplicate reports are caught before the reporter hits submit.

### Credential leaks caught before they happen

A user pastes their deploy command into a bug report. It includes a database URL with credentials. That URL is now in your public issue tracker. Cluvo&apos;s built-in sanitizer strips common patterns (tokens, API keys, home directory paths) before the user sees the preview. You can add custom rules for project-specific patterns. It&apos;s not a guarantee that nothing sensitive slips through, but the defaults catch the most common leaks.

### No infrastructure, no maintenance burden

There&apos;s no server to host, monitor, or secure. The SDK is a dependency you add to your project, not an infrastructure commitment.

---

## Get Started

```bash
npm install @cluvo/sdk
```

Source and documentation: [github.com/syi0808/cluvo](https://github.com/syi0808/cluvo)

If you maintain a CLI and want higher-signal bug reports without standing up infrastructure, Cluvo is one approach worth trying.</content:encoded></item><item><title>Stop Wrestling with npm publish and cargo publish: A Better Way to Release JS+Rust Projects</title><link>https://blog.castle-yein.com/js-rust-publish-with-pubm/</link><guid isPermaLink="true">https://blog.castle-yein.com/js-rust-publish-with-pubm/</guid><description>Publishing JS+Rust projects means two auth systems, mismatched versions, and no rollback. Here&apos;s why existing tools fall short and how pubm fixes it.</description><pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate><content:encoded>You run `npm publish`. It succeeds. You run `cargo publish`. CI reports a timeout while waiting for crates.io to respond. Now you&apos;re in an ambiguous state: did the publish actually go through or not? Your `Cargo.toml` already says `1.2.4` and that commit is tagged and pushed.

Here&apos;s the problem: crates.io publishes are permanent. You can&apos;t delete a crate version, only yank it. If you retry, crates.io may reject it as a duplicate. If you don&apos;t, JS and Rust consumers could end up on different versions of what&apos;s supposed to be the same release.

It gets worse the more registries you maintain.

---

## The Rust-in-JS Wave Is Real and Growing

Rust-based JS tooling is no longer a niche experiment. The download numbers are hard to ignore:

- [@swc/core](https://npmjs.com/package/@swc/core), [@biomejs/biome](https://npmjs.com/package/@biomejs/biome), [@rspack/core](https://npmjs.com/package/@rspack/core), [oxlint](https://npmjs.com/package/oxlint): each pulling millions of weekly downloads on npm.

[napi-rs](https://github.com/napi-rs/napi-rs), the framework most of these tools are built on, has thousands of GitHub stars and [thousands of dependent projects](https://github.com/napi-rs/napi-rs/network/dependents). Rust has topped the [most admired programming language](https://survey.stackoverflow.co/2024/technology#admired-and-desired) ranking in the Stack Overflow Developer Survey every year since 2016.

Evan You raised [$12.5M in Series A funding](https://voidzero.dev/posts/announcing-voidzero-inc) through VoidZero in October 2025 to build a unified Rust-based JS toolchain. [wasm-pack will be archived in September 2025](https://blog.rust-lang.org/inside-rust/2025/07/21/sunsetting-the-rustwasm-github-org/), and [napi-rs v3 now offers its own WASM compilation support](https://napi.rs/blog/announce-v3) as an alternative. More JS packages are shipping Rust internals.

That growth creates a publish problem that existing tools only partially address.

---

## Why Publishing JS+Rust Is Uniquely Painful

A typical npm-only release is annoying. A JS+Rust release is a different category of problem.

### The platform matrix

napi-rs compiles native binaries for each target platform. One release means building for Linux x64, Linux arm64, macOS x64, macOS arm64, Windows x64, and more. The [napi-rs package-template](https://github.com/napi-rs/package-template) CI file is 385 lines long, covers 13 platform targets, and generates 13+ platform-specific npm packages per release. Each of those packages needs to be published individually.

One version bump. Thirteen-plus npm packages. One crate. Two completely separate auth systems.

### Two auth systems with different semantics

npm uses `NPM_TOKEN`. crates.io uses `CARGO_REGISTRY_TOKEN`. They&apos;re configured differently, stored differently, and expire differently. If either token is missing or stale, you discover it mid-release.

npm has an unpublish policy, but it&apos;s not a free pass. Even within the 72-hour window, the same `name@version` combination can never be reused once unpublished. crates.io has no unpublish at all. You yank to mark a version as broken, but it stays on the registry forever. Neither registry gives you a clean rollback, and their policies don&apos;t align. A failed mid-flight release leaves asymmetric damage across registries.

### The script problem

Here&apos;s what a real manual release script looks like for an napi-rs project:

```bash
# 1. Bump version in package.json and all platform packages
npm version patch --workspaces
# 2. Update Cargo.toml version manually
# Edit: version = &quot;1.2.4&quot;
# 3. Update Cargo.lock
cargo update --workspace
# 4. Commit everything
git add -A &amp;&amp; git commit -m &quot;chore: release v1.2.4&quot;
# 5. Tag
git tag v1.2.4
# 6. Build all platform targets (this is what CI handles)
# 7. Publish all platform packages to npm
npm publish --workspace packages/linux-x64-gnu
npm publish --workspace packages/linux-arm64-gnu
# ... 12 more
# 8. Publish the main package
npm publish
# 9. Publish to crates.io
cargo publish
# 10. Push tag
git push origin main --tags
```

This doesn&apos;t include error handling. Add that and you&apos;re at 50+ lines. One developer described the experience in [napi-rs discussion #2087](https://github.com/orgs/napi-rs/discussions/2087): &quot;It took me a while to understand how to publish it to multiple platforms and the docs / tooling of this project needs a lot of improvement about that.&quot;

The response from another developer in the same thread: &quot;You just saved my life!&quot;

That exchange captures the state of the tooling. The knowledge is spread across discussions, gists, and blog posts. There&apos;s no standard tool.

---

## What Existing Tools Don&apos;t Cover

Existing tools solve parts of the release workflow well, but finding one that treats mixed JS+Rust publishing as a single coordinated release is surprisingly difficult.

| Tool | Ecosystem | crates.io Support | Multi-Registry | Rollback |
|---|---|---|---|---|
| cargo-release | Rust only | Yes | No | No |
| release-plz | Rust only, PR-based | Yes | No | No |
| np | npm only | No | No | No |
| release-it | npm only | No | No | No |
| semantic-release | npm only | Plugin (separate config) | Partial | No |
| changesets | npm only | No | No | No |
| release-please | npm + Rust plugin | Limited in mixed workspaces | Partial | No |
| **pubm** | **npm + JSR + crates.io** | **Yes** | **Yes** | **Best-effort** |

semantic-release gets closest with the [semantic-release-cargo](https://github.com/semantic-release-cargo/semantic-release-cargo) community plugin, but you&apos;re maintaining two separate config systems that don&apos;t share state. If the plugin lags behind crates.io API changes, you debug it yourself.

release-please has a Rust plugin, but [issue #2207](https://github.com/googleapis/release-please/issues/2207) (open since January 2024, triaged to P3) shows that mixed Node.js + Rust workspace scenarios still have gaps. This is not a criticism of those tools. They were designed for one ecosystem, and retrofitting multi-registry support into a single-registry model is a hard problem.

As Orhun Parmaksiz [put it](https://blog.orhun.dev/automated-rust-releases/) while surveying Rust release tooling: &quot;It is still too much manual work + release process is still not fully automated.&quot;

---

## How pubm Solves the JS+Rust Publish Problem

[pubm](https://github.com/syi0808/pubm) was built for exactly this scenario. The design premise is that a JS+Rust project should have one release command that handles both ecosystems without manual coordination.

### Zero-config registry detection

pubm reads your project&apos;s manifest files and infers what to publish:

- `package.json` present: publish to npm
- `jsr.json` present: publish to JSR
- `Cargo.toml` present: publish to crates.io
- All three present: publish to all three, in one pipeline

No config file is required for a standard setup. pubm detects Cargo workspaces and pnpm/yarn/npm/bun workspaces automatically.

### Preflight checks before anything changes

Before pubm touches a single file, it validates:

- You&apos;re on the configured release branch
- Your working tree is clean
- Your auth tokens are valid for every target registry
- You have publish permission for the specific package names

An expired `CARGO_REGISTRY_TOKEN` surfaces here, before the version is bumped, before anything is published. Not mid-flight.

### The publish pipeline

pubm runs releases as an ordered pipeline:

1. Prerequisites check (branch, clean tree, remote reachable)
2. Auth pre-validation against live registries
3. Version prompt (skipped in CI)
4. Test and build
5. Version bump (all manifests synced in one operation, git commit + tag)
6. Publish to all registries
7. Post-publish (push tag, create GitHub release draft)
8. Rollback on any failure

### Best-effort rollback

If any step fails, pubm reverses what it can:

- Deletes any git tags it created
- Reverts the version bump commit
- Restores all manifest files to their pre-release state
- Restores lock files

Your local repository ends up where it started. No orphaned tags, no half-bumped manifests. Registry-side rollback is best-effort: if `cargo publish` already completed before a later step fails, pubm cannot undo that publish (crates.io has no delete API). But your local state stays clean, so you can diagnose and decide what to do next without a corrupted working tree.

### Quick setup

```bash
npm i -g pubm
pubm init
```

`pubm init` runs an interactive wizard that detects your registries, configures your release branch, and optionally generates a CI workflow. After that, releases are one command:

```bash
pubm patch
# or: pubm minor / pubm major / pubm (interactive)
```

---

## Before and After

**Before pubm: a manual JS+Rust release (abbreviated)**

```bash
#!/usr/bin/env bash
set -e

VERSION=$1
if [ -z &quot;$VERSION&quot; ]; then
  echo &quot;Usage: $0 &lt;version&gt;&quot;
  exit 1
fi

# Check auth
npm whoami || { echo &quot;npm auth failed&quot;; exit 1; }
cargo publish --dry-run || { echo &quot;cargo package validation failed&quot;; exit 1; }

# Bump versions
npm version &quot;$VERSION&quot; --workspaces --no-git-tag-version
npm version &quot;$VERSION&quot; --no-git-tag-version
sed -i &quot;s/^version = .*/version = \&quot;$VERSION\&quot;/&quot; Cargo.toml
cargo update --workspace

# Commit and tag
git add -A
git commit -m &quot;chore: release v$VERSION&quot;
git tag &quot;v$VERSION&quot;

# Publish npm packages (platform packages first)
for pkg in packages/*/; do
  npm publish &quot;$pkg&quot; --access public || {
    echo &quot;Failed to publish $pkg&quot;
    git tag -d &quot;v$VERSION&quot;
    git reset --hard HEAD~1
    exit 1
  }
done
npm publish --access public

# Publish to crates.io
cargo publish || {
  # npm versions already live, can&apos;t unpublish
  echo &quot;cargo publish failed. npm packages are already published.&quot;
  echo &quot;Manually yank crates.io if needed.&quot;
  exit 1
}

git push origin main --tags
echo &quot;Released v$VERSION&quot;
```

That&apos;s 50+ lines, and the error handling in the cargo failure case is just a message. You can&apos;t unpublish what&apos;s already on npm.

**After pubm**

```bash
pubm patch
```

pubm handles every step in the before script, adds preflight auth validation that catches the cargo failure before any publish, and provides best-effort rollback instead of a manual cleanup message.

---

## What pubm Doesn&apos;t Solve

No tool can fully undo a completed registry publish. It&apos;s worth being upfront about what falls outside pubm&apos;s scope:

- **crates.io publish is irreversible.** Once a version lands on crates.io, pubm cannot remove it. At that point, you may choose to yank it manually, while pubm focuses on restoring your local git and manifest state.
- **Registry-side partial success is not fully recoverable.** If npm packages are already live and `cargo publish` fails, pubm rolls back git state but cannot unpublish what&apos;s already on npm (same limitation as any other tool).
- **Complex monorepo structures may need additional configuration.** pubm auto-detects standard workspace layouts, but unusual nested or multi-root setups may require explicit configuration in `pubm.config.ts`.

Acknowledging these boundaries is part of the design. pubm focuses on preventing these situations through preflight checks rather than promising to fix them after the fact.

---

## Get Started

The next time `cargo publish` fails halfway through, you won&apos;t be staring at mismatched registry versions. If you maintain a project that ships to both npm and crates.io, try pubm:

```bash
npm i -g pubm
pubm init
pubm
```

Full documentation and CI setup guides are at [syi0808.github.io/pubm](https://syi0808.github.io/pubm/guides/quick-start/). The source is on [GitHub](https://github.com/syi0808/pubm).</content:encoded></item><item><title>Manual Publishing vs pubm: The Reality of npm + JSR Multi-Registry Releases</title><link>https://blog.castle-yein.com/manual-vs-pubm-multi-registry-publishing/</link><guid isPermaLink="true">https://blog.castle-yein.com/manual-vs-pubm-multi-registry-publishing/</guid><description>Why publishing to npm and JSR manually breaks in predictable ways, how existing tools fall short, and what pubm does differently with atomic rollback.</description><pubDate>Wed, 25 Mar 2026 00:00:00 GMT</pubDate><content:encoded>You&apos;re cutting a release. You run `npm version patch`, execute `npm publish`, watch the success message scroll past, then run `npx jsr publish`. JSR rejects the package. A slow types violation.

npm has `1.2.4`. JSR is still on `1.2.3`. Your `package.json` says `1.2.4` and that commit is already tagged. You have three choices: deprecate the npm release, burn a `1.2.5` that fixes nothing, or leave the registries permanently out of sync.

This is the core problem with multi-registry npm and JSR publishing today: the two registries have no shared tooling, no coordinated auth flow, and no rollback mechanism. You&apos;re duct-taping two independent operations together and hoping both succeed.

---

## Why JSR Matters Now

JSR launched open beta in early 2024, built by the Deno team as a TypeScript-first alternative to npm. The growth has been fast. By mid-2024, JSR was seeing roughly 250 new packages per week. By January 2025, that number had risen to 400 per week.

The package list tells a clearer story than the numbers. The OpenAI JavaScript SDK published to JSR in January 2025 as `@openai/openai`. Hono published `@hono/hono` in June 2024. Deno&apos;s standard library moved entirely to JSR. Valibot is there. These are libraries with serious production usage, not experimental adopters testing the waters.

JSR fills real gaps that npm has never addressed, while npm&apos;s 2.5 million packages and over 180 billion downloads per month continue growing on their own track:

- You publish `.ts` source directly. JSR handles transpilation.
- API documentation is auto-generated from your TypeScript types and versioned per release.
- JSR Score gives contributors a concrete quality checklist.
- CI publishing uses OIDC tokens instead of long-lived secrets.
- Packages work with npm, pnpm, yarn, and bun, though npm, bun, and older yarn/pnpm versions require a `.npmrc` entry (added by `npx jsr add`). Newer pnpm (10.9+) and Yarn (4.9+) have native JSR support. npm and yarn may produce duplicate installations where pnpm does not.

As Theo Browne put it: &quot;I can&apos;t honestly remember the last time the npm registry shipped a meaningful new feature.&quot; JSR has a different focus entirely, and it&apos;s finding an audience.

If your library targets TypeScript developers, shipping to JSR is increasingly an expectation.

---

## Manual Publishing: What It Actually Looks Like

The real 8-step workflow for publishing a package to both npm and JSR manually:

```bash
# 1. Bump version in package.json (without auto-commit/tag)
npm --no-git-tag-version version patch

# 2. Manually update jsr.json to match (npm version does NOT touch this file)
# Edit jsr.json: &quot;version&quot;: &quot;1.2.4&quot;

# 3. Run tests
npm test

# 4. Build
npm run build

# 5. Publish to npm
npm publish

# 6. Publish to JSR
npx jsr publish

# 7. Commit, tag, and push
git add package.json jsr.json
git commit -m &quot;v1.2.4&quot;
git tag v1.2.4
git push origin main --tags
```

Step 1 needs the `--no-git-tag-version` flag because `npm version` by default creates a commit and tag immediately, before you&apos;ve had a chance to update `jsr.json`. Without the flag, step 2&apos;s change would sit outside the release commit entirely, and step 7 would try to create a tag that already exists.

Step 2 is the other trap. `npm version` does not touch `jsr.json`. That file is outside npm&apos;s awareness entirely. If you forget to update it manually, you publish mismatched versions: npm gets `1.2.4`, JSR gets `1.2.3` wrapped in a `1.2.3` manifest that now holds `1.2.4` code.

Three failure scenarios happen regularly in practice:

**Partial publish.** npm accepts `1.2.4`. JSR fails due to a &quot;slow types&quot; violation (a JSR-specific TypeScript check). npm&apos;s unpublish window is 72 hours and leaves behind a tombstone. You cannot republish `1.2.4` to npm. Your registries are now permanently out of step unless you burn a patch version.

**Auth failure mid-flow.** Your JSR token expired last week. You don&apos;t discover this until step 6, after npm already has the release. You&apos;re authenticated to npm but not JSR. The release is half done.

**Version drift over time.** You publish to npm consistently but skip JSR occasionally because the auth setup is tedious. Over months, JSR falls two or three versions behind. Users who install from JSR get stale code and file bugs against a version you&apos;ve already patched.

Changesets&apos; own documentation describes `changeset publish` as publishing to npm and creating git tags, with no mention of JSR. In issue #1717 (August 2025), a user requested first-class support, noting that &quot;`changeset publish` currently only supports `npm publish`&quot; and that &quot;having official guidance or hooks for this would reduce the amount of custom scripting needed in CI.&quot; As of August 2025, the issue had no maintainer response, assignee, or linked PR.

---

## Why Existing Tools Don&apos;t Solve This

The tools you already use don&apos;t have built-in multi-registry publishing.

| Tool | Weekly Downloads | JSR Support | Multi-Registry | Rollback |
|---|---|---|---|---|
| semantic-release | 2.44M | Community plugin only | No | No |
| release-it | 815K | Not built-in (possible via hooks/plugins) | No built-in | No |
| np | 143K | None | No | npm only |
| Changesets | — | None | No | No |
| pubm | new | Native | Yes (npm + JSR + crates.io) | Yes, repo-state rollback |

semantic-release is the closest to feature-complete, but its JSR support depends on a community plugin outside the core project. You configure it, it breaks when the plugin lags behind JSR API changes, you debug it, repeat.

release-it has no built-in JSR workflow at the time of writing. Its hooks and plugin system could be configured to call `npx jsr publish`, but there is no first-class JSR integration in core. release-please, another popular option, requires manual `extra-files` and `jsonpath` configuration to keep `jsr.json` in sync. That setup works, but it&apos;s fragile: the version sync logic lives in config files you maintain, not in the tool.

np is focused entirely on npm. It adds safety checks and a nice interactive prompt, but it has no concept of secondary registries.

JSR didn&apos;t exist when these tools were designed, and retrofitting multi-registry support into a tool built around npm&apos;s single-registry model is harder than building it from scratch.

pubm is the only tool in this space with native multi-registry support and repo-state rollback built into its core pipeline.

---

## Publishing with pubm

The install and setup is three commands:

```bash
npm i -g pubm
pubm init
pubm
```

`pubm init` runs an interactive wizard that detects your registries, configures your preferred branch, sets up changelog options, and optionally generates a CI workflow file. After that, `pubm` is your release command.

When you run `pubm`, you get an interactive version prompt:

```
? Select version bump:
  patch  (1.2.3 → 1.2.4)
  minor  (1.2.3 → 1.3.0)
❯ major  (1.2.3 → 2.0.0)
  custom
```

If you prefer to skip the prompt: `pubm patch`, `pubm minor`, or `pubm major`.

The same workflow with pubm:

| Manual Step | pubm Equivalent |
|---|---|
| `npm --no-git-tag-version version patch` | handled internally |
| Edit `jsr.json` manually | handled internally, both files synced atomically |
| `npm test` | runs your `test` script automatically |
| `npm run build` | runs your `build` script automatically |
| `npm publish` | handled internally |
| `npx jsr publish` | handled internally |
| `git add . &amp;&amp; git commit &amp;&amp; git tag v1.2.4` | handled internally |
| `git push origin main --tags` | handled internally |

Zero-config registry detection removes the setup overhead. pubm reads your project&apos;s manifest files:

- `package.json` present: publish to npm
- `jsr.json` present: publish to JSR
- Both present: publish to both, automatically

No config file is required. If you need to override defaults (custom registry URLs, pre/post publish hooks, or non-standard script names), `pubm.config.ts` handles that, but you won&apos;t need it for a standard npm + JSR setup.

---

## The 8-Step Pipeline

pubm runs its release as an ordered pipeline with a clear responsibility at each step:

1. **Prerequisites check.** Validates you&apos;re on the correct branch, your working tree is clean, and your remote is reachable.

2. **Auth pre-validation.** Pings npm and JSR to confirm your credentials are valid and you have publish permission for the specific package name. This happens before any files are modified.

3. **Version and tag prompts.** Interactive version selection. Automatically skipped in CI environments.

4. **Test and build.** Runs your configured `test` and `build` scripts. A test failure aborts the release here, before anything is published.

5. **Version bump.** Updates `package.json` and `jsr.json` to the new version in a single operation, then creates a git commit and tag.

6. **Publish.** Publishes concurrently to npm and JSR.

7. **Post-publish.** Pushes the git tag to your remote and creates a GitHub release draft.

8. **Rollback on failure.** If any prior step fails, pubm reverses everything it completed locally (git tags, version commits, manifest files). Registry-side publishes that already succeeded cannot be undone.

Step 2 is the one that matters most for the partial publish problem. Auth is validated against live registries before pubm touches a single file. You cannot reach the publish step without confirmed credentials. The scenario where npm succeeds and JSR fails due to an expired token is eliminated: that failure surfaces in step 2, before the version is bumped, before anything is published, before you have anything to undo. Publish-time failures (network issues, JSR validation errors) can still cause one registry to succeed while the other fails, but pre-validation catches the most common class of problems.

---

## Repo-State Rollback

What pubm reverses on failure (local repo state only; registries cannot be rolled back once a publish succeeds):

- Deletes any git tags it created
- Reverts the version bump commit
- Restores `package.json` and `jsr.json` to their pre-release versions
- Restores lock files

The result is a clean working tree with no orphaned tags and no version mismatch between your manifest files. You end up exactly where you started locally.

Note: if one registry publish succeeds before the other fails, the successful publish cannot be reversed. npm does not allow republishing a version even after unpublish, and both registries have their own constraints. pubm&apos;s rollback covers your repo state so you can diagnose and retry cleanly, but it is not a cross-registry atomic transaction.

Compare this with the manual failure scenario:

| Scenario | Manual | pubm |
|---|---|---|
| JSR &quot;slow types&quot; rejection during concurrent publish | npm has 1.2.4, JSR on 1.2.3, git tag exists | Publish-time failure triggers repo-state rollback: git tag deleted, version commit reverted, manifests restored. npm publish may already have succeeded and cannot be reversed. |
| Expired JSR token discovered mid-publish | npm published, auth error on JSR, inconsistent state | Caught in step 2 before any publish; nothing to undo |
| Test failure after version bump | Version bumped and tagged, tests never ran | Tests run in step 4; failure before version bump |
| Network failure during JSR publish | Partial upload, unknown registry state | Repo-state rollback reverses version commit and tag; npm may already have the version |

Rollback is tracked per-operation. If a failure occurs before publish, there&apos;s nothing to reverse on the registry side. pubm only reverses what it actually completed.

---

## Dry Run and CI

Before setting up CI, validate your full pipeline locally without side effects:

```bash
pubm --dry-run
```

This runs every step, including auth validation, test, build, and a simulated publish, but nothing is written to registries and no git commits are created. The version bump rolls back automatically.

For CI, pubm operates in headless mode. Set your tokens as environment variables and run:

```bash
pubm --mode ci --phase publish
```

The `--phase publish` flag tells pubm to skip interactive prompts and run only the publish pipeline. It assumes version bumping and tagging were already handled (e.g., by the tag push that triggered the workflow).

A complete GitHub Actions workflow:

```yaml
name: Publish

on:
  push:
    tags:
      - &apos;v*&apos;

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: &apos;22&apos;
          registry-url: &apos;https://registry.npmjs.org&apos;
      - run: npm ci
      - name: Publish
        run: pubm --mode ci --phase publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
          JSR_TOKEN: ${{ secrets.JSR_TOKEN }}
```

The `id-token: write` permission enables OIDC-based JSR authentication if you prefer that over a static token. Note: OIDC requires that your JSR package is first linked to the GitHub repository in JSR&apos;s settings. `id-token: write` alone is not sufficient.

---

## What pubm Gives You

JSR is growing. The libraries that matter are publishing there. Manual workflows break in predictable ways, and the existing tools weren&apos;t designed for this problem.

pubm addresses it at the pipeline level:

1. **Zero-config registry inference.** Having `jsr.json` in your project is enough. No plugin configuration, no extra files.
2. **Auth pre-validation before any side effects.** Credentials are confirmed before files are modified, eliminating the most common class of partial publish failures.
3. **Repo-state rollback on failure.** A failed release reverts your local repo (git tags, version commits, manifest files) to the state before you started. Registry-side publishes that already succeeded are not reversible, but pre-validation keeps that window narrow.
4. **AI coding agent support.** If you use Claude Code, Codex CLI, or Gemini CLI, `pubm setup-skills` installs agent skills so you can configure and run pubm directly from your agent.

```bash
npm i -g pubm
pubm init
pubm
```

**Docs and full reference:** [pubm docs](https://syi0808.github.io/pubm/guides/quick-start/)</content:encoded></item></channel></rss>