← Back to blog

Publishing pi-persistent-intelligence taught me that npm and GitHub Packages only look similar from a distance. They are different registries with different authentication paths, package naming rules, provenance options, and failure modes. Treating them as one destination is how releases get messy.

My goal was a release workflow where one tag could build, test, typecheck, and publish the same package to both registries without hiding the difference between them. If npm succeeds and GitHub Packages fails, I want to know that. If provenance is missing, I want the release to make that visible. If the version is wrong, I want the workflow to stop before publishing.

The release flow treats npmjs.com and GitHub Packages as separate targets with separate credentials and verification.

The first mistake was assuming the GitHub Packages tab meant npm

Publishing to npmjs.com does not populate GitHub Packages. The GitHub repository Packages tab only shows packages published to npm.pkg.github.com. That matters when a package is scoped, because the name, registry, and token must all agree. A package can be correctly published to npm and still be absent from GitHub Packages.

Once I treated the registries as separate targets, the workflow became clearer. The npm job uses npm credentials or trusted publishing. The GitHub Packages job uses the GitHub registry and the GitHub token path. Both jobs run after the same verification gate, but they publish independently.

release-gate.txt
bun test
bun run typecheck
npm publish --access public --provenance
npm publish --registry https://npm.pkg.github.com

Version discipline mattered more than automation

Automation can make bad release habits faster. I added a rule for myself: one coherent feature set gets one version. During early package work it is tempting to bump versions repeatedly in one day because every small fix feels important. That creates confusing package history and makes the project look less stable than it is.

The package is still pre-stable, so the version should communicate that honestly. A 0.x package can move quickly, but the release history should still be readable. The changelog should explain what changed for users, not narrate every development decision.

What the workflow checks

The release path checks tests, TypeScript, package metadata, registry configuration, and publish commands. The repository also keeps user-facing documentation separate from private development notes. Public README content explains installation, usage, trust boundaries, and package behavior. Internal architecture rationale stays in private notes until it becomes documentation a user actually needs.

That separation made the package easier to present. Users do not need my whole dev diary. They need to know what the package does, how to install it, how memory is stored, what leaves the machine, and how to inspect or remove data.

The practical lesson

The difficult part of publishing was not writing YAML. It was understanding what each registry expects and refusing to blur the boundaries. The final workflow is not glamorous, but it is predictable. A tag builds the package, gates it, publishes it, and leaves a trail that can be checked later.

Next post →
© 2026 mont3llAvailable for work
loading portfolio000%