How to ship a new version of @rousan/mx, and the gotchas caught the hard way that aren't obvious from code.
Releases are CI-driven: every push to main (i.e. every merged PR) runs .github/workflows/release.yml, which publishes whatever version sits in npm/package.json.
The contract: every merge to main must produce a new release. The PR is responsible for bumping npm/package.json's "version". The workflow guards on this — if the version still matches an existing vX.Y.Z tag, the run fails with an error telling you to bump the version. So:
# in your PR branch, before merging:
$EDITOR npm/package.json # bump "version"
git commit -am "release vX.Y.Z"
# open PR → merge to main → CI releases automaticallyWhat the workflow does on each push to main:
- Checkout with full history + tags.
- Resolve
versionfromnpm/package.json; fail ifvX.Y.Zalready exists as a tag (locally or on origin) — bump the version and re-merge. pnpm install --frozen-lockfile, thenpnpm typecheck && pnpm lint && pnpm test && pnpm build.npm publishfromnpm/, authenticated via theNPM_TOKENsecret (NODE_AUTH_TOKEN).- Create + push the annotated
vX.Y.Ztag. - Create a GitHub Release for the tag with auto-generated notes.
CI cannot use the interactive --auth-type=web browser flow. It needs an npm automation token (automation tokens bypass 2FA at publish time):
- Sign in at https://www.npmjs.com as a user with publish rights on the
@rousanorg. - Avatar → Access Tokens → Generate New Token → Classic Token → type Automation (or a Granular Access Token scoped to publish
@rousan/mx). Copy it — it's shown once. - In GitHub: repo Settings → Secrets and variables → Actions → New repository secret. Name it
NPM_TOKEN, paste the value.
GITHUB_TOKEN (used to push the tag and create the Release) is provided automatically by Actions; the workflow requests contents: write permission for it. No other secret is needed.
scripts/release.sh (pnpm release) still works for local/emergency publishing when CI is unavailable. It is superseded by the workflow for normal releases — prefer merging to main. One-time on the publisher's machine:
npm loginTo cut a release manually:
$EDITOR npm/package.json # bump "version"
git commit -am "release vX.Y.Z"
pnpm release # → scripts/release.shpnpm release (interactive — must run in a real terminal, not via stdin pipe):
- Verify
npm whoami— must be logged in. - Working tree clean — refuses with
git statusshort output if not. - Tag doesn't exist locally or on origin (with a fresh
git fetch --tags). - Version not already published —
npm view @rousan/mx@X.Y.Zreturns 404. - Run
pnpm typecheck && pnpm lint && pnpm test && pnpm build— all green. - Show
npm pack --dry-runtarball preview. - Prompt:
Publish @rousan/mx@X.Y.Z and tag vX.Y.Z? (y/N)— typey. npm publish --auth-type=webfromnpm/— opens a browser, you confirm in browser (handles 2FA cleanly).git tag -a vX.Y.Z -m vX.Y.Z,git push origin HEAD,git push origin vX.Y.Z.
The script fails fast on any preflight issue — safe to re-run.
For a brand-new scoped package (@org/pkg), the org must exist on npm before you can publish. The npm CLI has npm org set / rm / ls but no create. Orgs are created via the web UI only: https://www.npmjs.com/org/create. Pick the Free plan (unlimited public packages, no card required).
This bit us when first publishing @rousan/mx@1.0.0. The publish errored with "Scope not found" until the @rousan org existed on npm.
npm has an opaque similarity heuristic that rejects unscoped names at publish time. mxcli was rejected at publish for being too similar to an existing mx-cli package, even though npm view mxcli had returned a clean 404.
Scoped names (@org/name) bypass this check entirely, which is why we settled on @rousan/mx after the mxcli attempt failed.
If your npm account has 2FA enabled (most do today), a plain npm publish triggers an interactive OTP prompt — which doesn't work cleanly when stdin is piped. The fix is npm publish --auth-type=web: opens a browser to confirm, no OTP typing needed. scripts/release.sh uses this flag.
After a successful publish to a brand-new scoped org, npm view @org/pkg may return 404 for several minutes while the npm CDN and search index catch up. The package is published — the version-specific endpoint shows up immediately:
curl -s https://registry.npmjs.org/@rousan/mx/latest | head -c 200This isn't an error; just propagation lag. Don't try to "fix" by republishing.
git push origin HEAD pushes whatever branch is currently checked out. Under self-hosting, the worktree is on a feature branch (improve-mx, etc.), not main. So the release commit lands on the feature branch, not main.
Options:
- (a) Fast-forward the feature branch to
maindirectly:git push origin <feature>:mainif it's a clean linear extension. Then the release commit + tag live onmain. - (b) Merge to
mainfirst: switch to a worktree onmain, merge the feature, thenpnpm releasefrom there.
The release script doesn't yet enforce "release from main only" — that's an open improvement.
The publishable package layout is npm/ (committed: package.json, README.md; built: bin/, templates/, LICENSE). Anything outside npm/ is invisible to the npm registry — including the source code at packages/ and apps/. If you add new runtime templates, ensure tsup's onSuccess copy step picks them up (apps/cli/tsup.config.ts). The npm/templates/ directory is gitignored — it's regenerated on every build.
If a release changes templates/CLAUDE.md, existing runtimes won't see it until the user runs:
npm i -g @rousan/mx@latest
mx syncmx sync (the re-stamp command, formerly mx update) is non-destructive — never modifies work.json, body files, or anything the user owns. But it does re-stamp <runtime>/CLAUDE.md from the new template. (mx update is now the CLI self-update command.)
Semver, loosely interpreted (mx is at 2.x, internal-use):
- Patch (
X.Y.Z+1) — bug fixes, doc-only changes, presentation tweaks, behaviour clarifications that don't change CLI surface or schema. - Minor (
X.Y+1.0) — new commands, new flags, additive porcelain fields, runtime CLAUDE.md template changes (since they're a deliberate contract update requiringmx sync). - Major (
X+1.0.0) — a change to the runtime layout version. The CLI major maps 1:1 to the runtime version it supports (CLI 3.x ⇄ runtime v3), so a layout migration is a major bump and ships a registeredmx migratestep. 2.0.0 was the first major (container repo layout +mx.jsongate); 3.0.0 centralized hooks into<runtime>/hooks/and addedrepo.json. Smaller breaking tweaks have historically ridden minor bumps (e.g.--allsemantics flipping in 1.9.0) since mx is internal-use; document them clearly in the commit message.
pnpm publishfailing → checknpm whoami, check the scope exists, check the name isn't similarity-rejected, check 2FA isn't blocking.- Tag pushed but npm shows old version → wait 5 minutes (CDN), then check the
/latestendpoint directly. mxfrom$PATHshows wrong version → that's the globally installed one; might be stale.npm i -g @rousan/mx@latestto refresh.pnpm mx versionshows the version fromnpm/package.json— the CLI reads its own version from the package.json at startup (since v1.0.1).