<?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>DEV Community: Yein Sung</title>
    <description>The latest articles on DEV Community by Yein Sung (@syi0808).</description>
    <link>https://dev.to/syi0808</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%2F1927584%2F09e4c60e-9c4c-4d87-a983-0d3fde6acb0e.png</url>
      <title>DEV Community: Yein Sung</title>
      <link>https://dev.to/syi0808</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/syi0808"/>
    <language>en</language>
    <item>
      <title>I Built a Spam-Comment Review Bot After Almost Moving a GitHub Issue Conversation to Telegram</title>
      <dc:creator>Yein Sung</dc:creator>
      <pubDate>Sun, 03 May 2026 11:36:38 +0000</pubDate>
      <link>https://dev.to/syi0808/i-built-a-spam-comment-review-bot-after-almost-moving-a-github-issue-conversation-to-telegram-ikf</link>
      <guid>https://dev.to/syi0808/i-built-a-spam-comment-review-bot-after-almost-moving-a-github-issue-conversation-to-telegram-ikf</guid>
      <description>&lt;p&gt;I maintain a small open-source project called pubm.&lt;/p&gt;

&lt;p&gt;pubm is a tool for complex publish and release workflows. Since the project is still small, I use GitHub issues as my planning system. Every feature idea, rough product thought, and future workflow becomes an&lt;br&gt;
issue.&lt;/p&gt;

&lt;p&gt;One of those issues was about release channels: stable, beta, rc, canary, nightly, and how pubm should treat them as first-class release workflows.&lt;/p&gt;

&lt;p&gt;Then someone commented.&lt;/p&gt;

&lt;p&gt;At first, it felt good. The comment sounded helpful. The person talked about testing, validating workflows, and helping me get better feedback on the project.&lt;/p&gt;

&lt;p&gt;I was excited.&lt;/p&gt;

&lt;p&gt;That detail matters. Small open-source projects do not get much attention. When someone shows up and says they might help, it is easy to lean forward too quickly. English is not my first language either, so I tend&lt;br&gt;
to give vague wording extra patience.&lt;/p&gt;

&lt;p&gt;I replied. Then the conversation continued.&lt;/p&gt;

&lt;p&gt;The more I read, the more something felt wrong. The replies were friendly, but broad. They sounded adjacent to the issue without really engaging with the release-channel design problem.&lt;/p&gt;

&lt;p&gt;Then the conversation started moving toward Telegram or email.&lt;/p&gt;

&lt;p&gt;That was the point where I stopped.&lt;/p&gt;

&lt;p&gt;I checked the account more carefully. I saw limited public activity, similar outreach patterns across repositories, and enough public context to make me uncomfortable treating it like a normal contributor&lt;br&gt;
conversation.&lt;/p&gt;

&lt;p&gt;This is the public review comment my bot posted on the issue:&lt;br&gt;
&lt;a href="https://github.com/syi0808/pubm/issues/36#issuecomment-4364206862" rel="noopener noreferrer"&gt;https://github.com/syi0808/pubm/issues/36#issuecomment-4364206862&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That comment became the first real case study for Get Out Spam.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem is suspicious GitHub comments
&lt;/h2&gt;

&lt;p&gt;A suspicious comment is not always dangerous by itself.&lt;/p&gt;

&lt;p&gt;The risk starts when the comment opens a trust path.&lt;/p&gt;

&lt;p&gt;A maintainer replies. The conversation moves from GitHub to Telegram, email, or another private channel. Later, the discussion may turn into collaborator access, package publishing permissions, CI access, test&lt;br&gt;
environment access, or release workflow help.&lt;/p&gt;

&lt;p&gt;For a small open-source project, this can happen with almost no process.&lt;/p&gt;

&lt;p&gt;There may be no security team. There may not even be a second maintainer. The issue tracker is the whole process.&lt;/p&gt;

&lt;p&gt;I wanted a tool that helps identify suspicious GitHub comments before that trust path starts.&lt;/p&gt;

&lt;p&gt;Not a tool that says “this person is bad.”&lt;/p&gt;

&lt;p&gt;A tool that says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This comment has public signals worth reviewing before you move off GitHub.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Get Out Spam does
&lt;/h2&gt;

&lt;p&gt;Get Out Spam is a spam-comment review bot for GitHub maintainers.&lt;/p&gt;

&lt;p&gt;More specifically, it is a GitHub App that helps maintainers review suspicious issue comments before replying, moving off-platform, or sharing access.&lt;/p&gt;

&lt;p&gt;GitHub App page:&lt;br&gt;
&lt;a href="https://github.com/apps/get-out-spam" rel="noopener noreferrer"&gt;https://github.com/apps/get-out-spam&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The current MVP checks public signals such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recently created accounts&lt;/li&gt;
&lt;li&gt;sparse public profile metadata&lt;/li&gt;
&lt;li&gt;broad repository comment activity&lt;/li&gt;
&lt;li&gt;similar public outreach patterns&lt;/li&gt;
&lt;li&gt;off-platform contact requests&lt;/li&gt;
&lt;li&gt;prior public moderation evidence, when available&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output is intentionally neutral.&lt;/p&gt;

&lt;p&gt;It does not say someone is a scammer. It does not claim intent. It does not auto-block or report anyone.&lt;/p&gt;

&lt;p&gt;The recommendation is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep the discussion on GitHub and ask for a concrete technical proposal before sharing private access or moving off-platform.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the sentence I wish I had seen earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it is a review bot, not a verdict bot
&lt;/h2&gt;

&lt;p&gt;A spam-comment review bot can easily become harmful if it speaks too strongly.&lt;/p&gt;

&lt;p&gt;New contributors can have sparse profiles. Non-native English speakers can write broad comments. Legitimate contributors sometimes ask for easier communication channels.&lt;/p&gt;

&lt;p&gt;So Get Out Spam is designed to help review suspicious comments, not to make final judgments about people.&lt;/p&gt;

&lt;p&gt;The bot comment in the pubm issue shows the shape I want:&lt;br&gt;
&lt;a href="https://github.com/syi0808/pubm/issues/36#issuecomment-4364206862" rel="noopener noreferrer"&gt;https://github.com/syi0808/pubm/issues/36#issuecomment-4364206862&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It names public signals, links to public evidence where possible, and says clearly that it is not a spam verdict.&lt;/p&gt;

&lt;p&gt;That constraint matters. The tool should help maintainers slow down, not encourage public shaming.&lt;/p&gt;

&lt;h2&gt;
  
  
  The maintainer moment I want to protect
&lt;/h2&gt;

&lt;p&gt;The most dangerous moment is not when a spam comment appears.&lt;/p&gt;

&lt;p&gt;It is when the maintainer wants it to be real.&lt;/p&gt;

&lt;p&gt;That was me.&lt;/p&gt;

&lt;p&gt;A small project got attention. Someone sounded helpful. I wanted to continue the conversation. I almost treated vague outreach as a real contributor path before checking the public context carefully.&lt;/p&gt;

&lt;p&gt;Get Out Spam exists to add friction at that exact moment.&lt;/p&gt;

&lt;p&gt;A good outcome looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A new account comments on an issue.&lt;/li&gt;
&lt;li&gt;The bot checks public comment and account signals.&lt;/li&gt;
&lt;li&gt;If the review level is high enough, it posts a neutral note.&lt;/li&gt;
&lt;li&gt;The maintainer keeps the discussion on GitHub.&lt;/li&gt;
&lt;li&gt;The maintainer asks for a concrete technical proposal before sharing access.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That would have helped me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current status
&lt;/h2&gt;

&lt;p&gt;Get Out Spam is still an MVP.&lt;/p&gt;

&lt;p&gt;The GitHub App page is public:&lt;br&gt;
&lt;a href="https://github.com/apps/get-out-spam" rel="noopener noreferrer"&gt;https://github.com/apps/get-out-spam&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The source repo is still private while I finish the public release checklist, policies, and beta testing. I do not want to publish a tool like this before the wording, correction process, and false-positive&lt;br&gt;
handling are in better shape.&lt;/p&gt;

&lt;p&gt;Right now, the app is already posting review comments in my own repo. The public example from pubm is here:&lt;br&gt;
&lt;a href="https://github.com/syi0808/pubm/issues/36#issuecomment-4364206862" rel="noopener noreferrer"&gt;https://github.com/syi0808/pubm/issues/36#issuecomment-4364206862&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project is meant for open-source maintainers, especially small projects that do not have a formal security or contributor review process.&lt;/p&gt;

</description>
      <category>maintainers</category>
      <category>opensource</category>
      <category>github</category>
      <category>security</category>
    </item>
    <item>
      <title>Why I Built pubm: One CLI to Publish to npm, JSR, and Beyond</title>
      <dc:creator>Yein Sung</dc:creator>
      <pubDate>Sat, 28 Mar 2026 05:29:56 +0000</pubDate>
      <link>https://dev.to/syi0808/why-i-built-pubm-one-cli-to-publish-to-npm-jsr-and-beyond-57kk</link>
      <guid>https://dev.to/syi0808/why-i-built-pubm-one-cli-to-publish-to-npm-jsr-and-beyond-57kk</guid>
      <description>&lt;p&gt;It was a Friday afternoon. &lt;code&gt;npm publish&lt;/code&gt; finished clean. Then I remembered I hadn't updated &lt;code&gt;jsr.json&lt;/code&gt;. Ran &lt;code&gt;npx jsr publish&lt;/code&gt;. Auth error, my JSR token had expired. So I refreshed it, ran again, and the version numbers between npm and JSR were now out of sync because I'd forgotten to bump &lt;code&gt;jsr.json&lt;/code&gt; before the first publish.&lt;/p&gt;

&lt;p&gt;That was the moment I started writing pubm. I began the project in 2024, got it to a working prototype, then life happened and it sat untouched for months. I picked it back up with Claude as a coding partner and brought it to completion. The architecture decisions and test coverage were mine to own (I wasn't going to hand that off), but having an AI pair helped me push through the implementation grind that had stalled the project the first time around.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Multi-Registry Publishing is a Manual Mess
&lt;/h2&gt;

&lt;p&gt;When JSR entered public beta in March 2024, it brought something genuinely useful: a TypeScript-first registry backed by Deno, with a governance board that includes Evan You, Isaac Schlueter, and Ryan Dahl. Given that lineup, JSR is here to stay, which means dual-publishing to npm and JSR isn't going away either. It's a complement, and a lot of packages now live on both.&lt;/p&gt;

&lt;p&gt;That's where the pain starts.&lt;/p&gt;

&lt;p&gt;Publishing to npm and JSR isn't twice the work of publishing to one. It's more like five times, because the two registries want completely different things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;npm&lt;/th&gt;
&lt;th&gt;JSR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Config file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jsr.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;npm token (&lt;code&gt;NODE_AUTH_TOKEN&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;GitHub OIDC or access token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What to publish&lt;/td&gt;
&lt;td&gt;Compiled JS&lt;/td&gt;
&lt;td&gt;TypeScript source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version bump&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Manual edit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publish command&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm publish&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npx jsr publish&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Manual steps multiply. And more manual steps means more things to forget or get wrong.&lt;/p&gt;

&lt;p&gt;The worst part isn't the individual failures. It's the partial failures. npm succeeds, JSR fails for some auth reason. Now you have version &lt;code&gt;1.2.3&lt;/code&gt; on npm but &lt;code&gt;1.2.2&lt;/code&gt; on JSR. How do you recover? You can't easily unpublish from npm (there's a 72-hour window, and it's still messy). So you publish &lt;code&gt;1.2.4&lt;/code&gt; on JSR to "catch up." Your changelog is now a lie.&lt;/p&gt;

&lt;p&gt;No existing tool handles this. I checked.&lt;/p&gt;




&lt;h2&gt;
  
  
  Existing Tools Don't Cover This Gap
&lt;/h2&gt;

&lt;p&gt;changesets, semantic-release, np, release-it. These are solid tools, and none of them claim to solve multi-registry publishing. They assume npm, full stop.&lt;/p&gt;

&lt;p&gt;changesets GitHub issue #1717 captures the problem well: the built-in publish command only supports npm, and anything else requires custom CI scripting. pnpm issue #8317 asks for &lt;code&gt;jsr.json&lt;/code&gt; version bumping support. The workarounds people use, &lt;code&gt;jsr2npm&lt;/code&gt;, &lt;code&gt;mirror-jsr-to-npm&lt;/code&gt;, prove the gap is real, but they're also hacks.&lt;/p&gt;

&lt;p&gt;The "official" solution today is: publish to npm via your release tool, then add &lt;code&gt;npx jsr publish&lt;/code&gt; as an extra CI step at the end. That's it. No version sync, no rollback if JSR fails after npm succeeds, no unified auth management.&lt;/p&gt;

&lt;p&gt;For a single package, this is annoying. For a monorepo with multiple packages going to multiple registries, it becomes genuinely difficult to keep straight.&lt;/p&gt;




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

&lt;p&gt;The goal was simple enough to say in one sentence: one command, every registry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pubm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole publish workflow. pubm detects your registries from your manifest files (&lt;code&gt;package.json&lt;/code&gt; → npm, &lt;code&gt;jsr.json&lt;/code&gt; → JSR, &lt;code&gt;Cargo.toml&lt;/code&gt; → crates.io), runs preflight checks, bumps versions across all config files in sync, and publishes to every registry in the right order.&lt;/p&gt;

&lt;p&gt;Here's pubm's own &lt;code&gt;pubm.config.ts&lt;/code&gt;, it publishes itself using itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pubm/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;brewTap&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pubm/plugin-brew&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;externalVersionSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pubm/plugin-external-version-sync&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&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="s2"&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;excludeRelease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;packages/pubm/platforms/*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;packages&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;packages/core&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;packages/pubm&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;packages/pubm/platforms/*&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;packages/plugins/plugin-external-version-sync&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;packages/plugins/plugin-brew&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="na"&gt;releaseAssets&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="na"&gt;packagePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;packages/pubm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;platforms/{platform}/bin/pubm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pubm-{platform}&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="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;brewTap&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Formula/pubm.rb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;packageName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pubm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;syi0808/homebrew-pubm&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="nf"&gt;externalVersionSync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;targets&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="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;website/src/i18n/landing.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/v&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.\d&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plugins/pubm-plugin/.claude-plugin/plugin.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;jsonPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;version&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="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.claude-plugin/marketplace.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;jsonPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;metadata.version&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="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.claude-plugin/marketplace.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;jsonPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plugins.0.version&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="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;packages/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&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;p&gt;Five packages, two plugins, binary assets, Homebrew formula, version strings synced across arbitrary files. One command.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preflight Checks
&lt;/h3&gt;

&lt;p&gt;Before pubm touches anything, it runs a preflight phase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Branch validation (are you on the right branch?)&lt;/li&gt;
&lt;li&gt;Clean working tree check&lt;/li&gt;
&lt;li&gt;Auth token validation for every registry you're publishing to&lt;/li&gt;
&lt;li&gt;Dry-run publish to catch packaging errors before anything goes out&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any preflight check fails, pubm stops. Nothing has been published, nothing has been committed. You fix the issue and try again.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI and Local: Same Config, Same Command
&lt;/h3&gt;

&lt;p&gt;Most teams end up with divergent local and CI release workflows. Local is ad-hoc; CI is automated but slightly different. pubm uses the same config and the same command in both environments, just with different phase flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# CI: split into two phases&lt;/span&gt;
pubm &lt;span class="nt"&gt;--mode&lt;/span&gt; ci &lt;span class="nt"&gt;--phase&lt;/span&gt; prepare
pubm &lt;span class="nt"&gt;--mode&lt;/span&gt; ci &lt;span class="nt"&gt;--phase&lt;/span&gt; publish

&lt;span class="c"&gt;# Local: all in one&lt;/span&gt;
pubm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Getting the config and CLI right was the easier half. The harder half was what happens when things go wrong mid-release.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hard Parts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Rollback
&lt;/h3&gt;

&lt;p&gt;The hardest design decision in rollback was figuring out what to do about registry unpublish. Some registries allow it in a time window; others don't at all. Making rollback feel safe without over-promising on things pubm can't undo took most of the iteration time.&lt;/p&gt;

&lt;p&gt;That's the problem from the top of this post made concrete: npm succeeds, JSR fails, and your changelog becomes a lie. The rollback system exists specifically to prevent that state.&lt;/p&gt;

&lt;p&gt;pubm's &lt;code&gt;RollbackTracker&lt;/code&gt; class records every action that succeeds during a release: version bump, git commit, git tag, npm publish, JSR publish, GitHub release, and so on. If any subsequent step fails, it reverses everything in LIFO order.&lt;/p&gt;

&lt;p&gt;Rollback on registry unpublish is best-effort with user confirmation, because some registries have restrictions. But the git operations (undo commit, delete tag) happen automatically. You end up back at the state before you ran pubm. No half-released versions, no changelog drift, no &lt;code&gt;1.2.4&lt;/code&gt; published on JSR just to catch up to npm.&lt;/p&gt;

&lt;p&gt;The rollback system is also SIGINT-safe. If you Ctrl+C during a publish, pubm catches the signal and runs the rollback before exiting. Actions that require confirmation are skipped in that case (you can't prompt interactively during a signal handler), but they're listed so you know what to clean up manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Plugin System
&lt;/h3&gt;

&lt;p&gt;npm and JSR are the obvious registries, but releasing software often involves more than that. Homebrew, GitHub Releases, binary tarballs, version strings embedded in documentation sites.&lt;/p&gt;

&lt;p&gt;pubm's plugin system hooks into every stage of the release lifecycle: build, version, publish, push, and asset packaging. Plugins can also register custom credentials, add preflight checks, register new registry types, and add CLI subcommands.&lt;/p&gt;

&lt;p&gt;Two official plugins ship with pubm:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@pubm/plugin-brew&lt;/code&gt;&lt;/strong&gt;: Publishes a Homebrew formula by opening a PR to your tap repo. If a subsequent step fails and pubm rolls back, the plugin closes the PR it opened. The rollback integrates with the same &lt;code&gt;RollbackTracker&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@pubm/plugin-external-version-sync&lt;/code&gt;&lt;/strong&gt;: Syncs the published version into arbitrary files, landing pages, plugin manifests, JSON configs, anywhere a version string needs to live. Uses regex patterns or JSON path selectors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registry Abstraction
&lt;/h3&gt;

&lt;p&gt;Getting the abstraction right for different registries was the most technically interesting design problem. Each registry has different auth protocols, different publish commands, different error shapes, different concepts of what "a package" even means.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;RegistryDescriptor&lt;/code&gt; pattern treats each registry as a data structure with a connector factory and a publish factory. Uniform interface, registry-specific implementations behind it. This is what makes it possible to add crates.io support or a private registry without rewriting the core publish loop.&lt;/p&gt;

&lt;p&gt;pubm also detects workspaces from pnpm, yarn, npm, bun, deno, and Cargo. For Rust crates, it builds a dependency graph and publishes in topological order, this matters because &lt;code&gt;crates.io&lt;/code&gt; rejects a crate if its dependencies haven't been published yet, and the error message won't tell you that's why.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Dogfooding has been the most valuable part of building pubm. Not because it's a good practice in the abstract, but because pubm publishes itself: 5 packages to npm (one of which also goes to JSR), plus Homebrew and GitHub Releases, on every release. That means every edge case in the rollback system, every auth flow, every registry interaction, I hit it myself before anyone else does. The rollback code got its best test cases from actual release failures during development, not from tests I wrote in advance.&lt;/p&gt;

&lt;p&gt;Getting the registry abstraction right took three rewrites. The first version had registry-specific error handling leaking directly into the core publish loop, adding JSR support meant touching files that should never have known JSR existed. The second version swung too far the other way: a deeply nested factory hierarchy that made adding crates.io harder than the first version. The third, with &lt;code&gt;RegistryDescriptor&lt;/code&gt; as a flat data structure, was the one that held.&lt;/p&gt;

&lt;p&gt;Preflight checks beat error messages. Every time. Running &lt;code&gt;pubm&lt;/code&gt; and having it stop cleanly before touching anything, because your auth token is expired, or your branch is wrong, is a completely different experience from getting a helpful error message after npm has already published.&lt;/p&gt;




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

&lt;p&gt;pubm is Apache 2.0, lives on GitHub at &lt;a href="https://github.com/syi0808/pubm" rel="noopener noreferrer"&gt;github.com/syi0808/pubm&lt;/a&gt;, and publishes to npm and JSR.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; pubm
&lt;span class="c"&gt;# or&lt;/span&gt;
brew tap syi0808/pubm &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; brew &lt;span class="nb"&gt;install &lt;/span&gt;pubm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you publish to more than one registry, give it a try. The &lt;a href="https://pubm.dev/guides/quick-start" rel="noopener noreferrer"&gt;quick-start guide&lt;/a&gt; walks through the setup in a few minutes: drop a &lt;code&gt;pubm.config.ts&lt;/code&gt; in your repo with your package paths, run &lt;code&gt;pubm&lt;/code&gt;, and see what the preflight phase catches. Even if you don't adopt the full workflow, the preflight checks alone have saved me several bad releases.&lt;/p&gt;

&lt;p&gt;Happy to hear feedback, bug reports, or "this completely broke my release pipeline" stories in the &lt;a href="https://github.com/syi0808/pubm/issues" rel="noopener noreferrer"&gt;GitHub issues&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>npm</category>
      <category>node</category>
      <category>deno</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
