{"slug":"publish-release","title":"publish-release","summary":"Release moi-computer to npm — verify locally, hand off to the gated GitHub Actions workflow, then verify the published package. Defaults to a `next` preview; pass `stable` for a `latest` release. Use when the user asks to publish, release, ship a version, or cut a dev preview.","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-08-24T16:57:41.800664Z","repo":{"url":"https://github.com/molefrog/moi","stars":173,"forks":12,"license":null,"updatedAt":"2026-09-27T12:58:26Z"},"bodyHtml":"<hr>\n<h2>name: publish-release\ndescription: Release moi-computer to npm — verify locally, hand off to the gated GitHub Actions workflow, then verify the published package. Defaults to a <code>next</code> preview; pass <code>stable</code> for a <code>latest</code> release. Use when the user asks to publish, release, ship a version, or cut a dev preview.</h2>\n<h1>Publish a moi-computer release</h1>\n<p><code>publish-release</code> — preview under the <code>next</code> dist-tag.\n<code>publish-release stable</code> — real release under <code>latest</code>, with a GitHub release.</p>\n<p><strong>npm publishing happens only in GitHub Actions.</strong> <code>.github/workflows/release.yml</code> authenticates\nvia npm trusted publishing (OIDC), gated behind the <code>release</code> environment's required reviewer.\nThere is no npm token anywhere and no npm session to reuse.</p>\n<ul>\n<li>Never run <code>npm publish</code> yourself. It will fail, and it is not the path.</li>\n<li>Never run <code>npm login</code> or ask the user for an OTP.</li>\n<li>A tag push is what starts a release. Tags are awkward to retract — earn the push with §2 first.</li>\n</ul>\n<p>Two human checkpoints: the user confirms the version before the push, and approves the\ndeployment in GitHub after it. Everything else runs unattended.</p>\n<h2>0. Preflight</h2>\n<ol>\n<li><p><code>git status</code> — clean tree, on <code>main</code>. Stop and ask if not.</p>\n</li>\n<li><p><code>git fetch &amp;&amp; git status -sb</code> — up to date with <code>origin/main</code>.</p>\n</li>\n<li><p><code>git log origin/main..HEAD --oneline</code> — if anything is unpushed, show it and confirm it should ship.</p>\n</li>\n<li><p>Record whether a dev <code>bun link</code> is active, because §8 branches on it:</p>\n<pre><code>readlink ~/.bun/install/global/node_modules/moi-computer\n</code></pre>\n<p>A path into this repo means a dev link that §8 must restore. No output (it is a real\ndirectory) means an ordinary global install; a missing path means a clean container with\nnothing to put back.</p>\n<p>Do <strong>not</strong> check <code>~/.bun/bin/moi</code> for this. That symlink reads\n<code>../install/global/node_modules/moi-computer/bin/moi.mjs</code> in <em>every</em> case — linked or\ninstalled — so it cannot answer the question §8 asks. Only the level below discriminates.</p>\n</li>\n</ol>\n<p>Both a cloud container and the user's machine run every phase below. The only difference is what\n§8 restores, which is why you captured it here rather than assuming.</p>\n<h2>1. Pick the version</h2>\n<p>Read the current state first — do not assume the file is a good starting point:\n<code>npm view moi-computer dist-tags</code> and the <code>version</code> in <code>package.json</code>.</p>\n<ul>\n<li><strong>next</strong>: if <code>package.json</code> already holds an <code>X.Y.Z-next.N</code> that sorts <em>above</em> the published\n<code>latest</code>, increment <code>N</code>. Otherwise start a fresh series at <code>&lt;next patch above latest&gt;-next.0</code>.\nThe <code>next</code> tag has drifted below <code>latest</code> before; a preview that installs older code than\nstable is a bug, so verify the new version sorts above <code>latest</code> before continuing.</li>\n<li><strong>stable</strong>: patch, minor, or major. If the intent is not obvious from the commits, ask.</li>\n</ul>\n<p>Do not use <code>npm version</code> — it makes its own commit and tag.</p>\n<h2>2. Verify locally, before touching the version</h2>\n<p>Everything here is read-only with respect to git. It duplicates the CI <code>verify</code> job on purpose:\nfailing here costs nothing, failing after the tag push costs a burned version.</p>\n<ol>\n<li><p><code>bun install --frozen-lockfile</code>, <code>bun run lint</code>, <code>bun run format:check</code>, <code>bun test</code>.</p>\n</li>\n<li><p><strong>Pack and inspect.</strong> Clear stale tarballs <em>first</em> — they accumulate in the repo root, and a\nbare <code>moi-computer-*.tgz</code> glob that matches more than one file makes <code>tar</code> read the first as\nthe archive and the rest as members to list. Every check then fails against a perfectly good\ntarball. Pin the exact filename instead of globbing:</p>\n<pre><code>rm -f moi-computer-*.tgz                                 # before packing, not after\nbun pm pack\nTGZ=$(ls moi-computer-*.tgz)                             # exactly one now\ntar -tzf \"$TGZ\" | grep -c '^package/dist/'               # must be &gt; 0\ntar -tzf \"$TGZ\" | grep -Ei '\\.env|secret'                # must be empty\n</code></pre>\n</li>\n<li><p><strong>Skim what is shipping</strong>: <code>git log &lt;last tag&gt;..HEAD --oneline</code>. Enough to describe the\nrelease and to know what §6 should poke at. Time-box it — a sanity check, not a QA pass.</p>\n</li>\n</ol>\n<p>Do <strong>not</strong> globally install the tarball here. It cannot coexist with a running dev server (see\n§6), and §6 smoke-tests the real published package anyway, which is better evidence. Delete the\ntarball once inspected.</p>\n<h2>3. Report and ask</h2>\n<p>Show the user, compactly: the version you propose, what is shipping since the last tag, and the\n§2 check results (lint, format, test counts, tarball <code>dist/</code> count, secrets scan). Then ask\nwhether to bump and push.</p>\n<p>There is no smoke output at this point — §2 deliberately skips the global install, and §6 does\nthe smoke against the published package. Do not go looking for it.</p>\n<p>Wait for a real answer. This is the checkpoint that gates the version number.</p>\n<h2>4. Bump, tag, push</h2>\n<pre><code># edit package.json version by hand\ngit commit -am \"Release vX.Y.Z\"\ngit tag vX.Y.Z\ngit push origin main vX.Y.Z          # push the tag by name; --tags would push every local tag\n</code></pre>\n<p>The commit fires <code>.githooks</code> (<code>oxlint --fix</code>, <code>oxfmt</code>). Normally they touch nothing, since only\n<code>package.json</code> changed — but if they do reformat files, those changes land in the release commit\nsilently. Check <code>git show --stat HEAD</code> and confirm it is the one-line version bump you expect.</p>\n<p>Then surface the approval link and stop. Do <strong>not</strong> trust <code>--limit 1</code> on its own: GitHub may not\nhave created the run yet when the push returns, so the newest run can still be the <em>previous</em>\nrelease. Match on the tag:</p>\n<pre><code>gh run list --workflow=release.yml --limit 5 \\\n  --json databaseId,url,status,headBranch \\\n  --jq '.[] | select(.headBranch == \"vX.Y.Z\")'\n</code></pre>\n<p>If that comes back empty, wait a few seconds and repeat until the run for your tag appears.\nGive the user that URL and tell them to approve the <code>release</code> environment. One link, one click.</p>\n<h2>5. Wait for the run</h2>\n<p><code>gh run watch &lt;id&gt; --exit-status</code>. It blocks until the user approves, so it can sit for a long\ntime — that is expected, not a hang. If it fails, read the logs before retrying.</p>\n<p>Recovery, if <code>verify</code> fails or the run is rejected:</p>\n<ul>\n<li>Same version again: delete the tag both places (<code>git tag -d vX.Y.Z</code>,\n<code>git push origin :refs/tags/vX.Y.Z</code>) before re-pushing.</li>\n<li>Re-run against a tag that is already correct: <code>gh workflow run release.yml -f tag=vX.Y.Z</code>.</li>\n</ul>\n<h2>6. Verify the published package</h2>\n<ol>\n<li><code>npm view moi-computer dist-tags</code> — the intended tag moved, and for a preview confirm\n<code>latest</code> did <strong>not</strong>.</li>\n<li><code>npm view moi-computer@&lt;version&gt; dist.attestations</code> — trusted publishing attaches provenance\nautomatically; its absence means the publish did not go through OIDC.</li>\n<li><strong>Smoke the published package.</strong> This needs the moi ports to itself: <code>CONTROL_PORT</code> defaults to\n<code>13059</code> (<code>server/constants.ts</code>) and <code>server/control.ts</code> binds it unconditionally, so <code>--port</code>\ndoes not let a second instance coexist. (<code>MOI_CONTROL_PORT</code> overrides it, but that is a test\nseam — smoke the real defaults.) Before installing:\n<ul>\n<li><code>lsof -ti:13059 -ti:13337</code> — if anything is running, it is almost certainly the user's own\n<code>bun run dev</code> in their terminal. Stop it, and <strong>tell the user you stopped it</strong>. Do not\nsilently respawn it in §8; a detached agent-owned supervisor is not the same thing and its\noutput goes where they are not looking.</li>\n<li><code>bun remove -g moi-computer</code> first. Installing over an existing install fails with\nENOENT/DependencyLoop because the global <code>package.json</code> pins the old range. Despite <code>-g</code>,\nthis still treats the <strong>current directory as a project</strong> and writes a <code>package.json</code> and\nlockfile there. Run it from the repo root (where both already exist and are git-tracked, so\nstray writes are visible) — never from <code>/tmp</code> or the scratchpad, where it leaves a junk\n<code>package.json</code> behind that will break <code>bun link</code> in §8.</li>\n<li><code>bun install -g moi-computer@&lt;version&gt;</code>, then <code>moi version</code>, <code>moi env</code>, <code>moi start</code>, and\ncurl <code>/</code> and <code>/api/workspaces</code>. Poke at anything §2.3 flagged as new, if it is quick.</li>\n</ul>\n</li>\n</ol>\n<p>If this fails, publishing cannot be undone cleanly. Unpublish is only possible within 72 hours\nand breaks anyone who already installed. The realistic remedy is <code>npm deprecate</code> on the bad\nversion plus a follow-up release. Tell the user rather than improvising.</p>\n<h2>7. Release notes — stable only</h2>\n<p><strong>Previews get no GitHub release and no release notes. Skip this entire section for <code>next</code>.</strong></p>\n<p>For a stable release:</p>\n<ol>\n<li>Read <code>.agents/rules/product-language.md</code> first — the notes are user-facing copy.</li>\n<li>Draft a minimal bullet changelog plus a compare link\n(<code>https://github.com/molefrog/moi/compare/vPREV...vX.Y.Z</code>). Behavior the user can observe,\nnot a file inventory.</li>\n<li>Show the draft and confirm before publishing. Revise if asked.</li>\n<li><code>gh release create vX.Y.Z --title \"vX.Y.Z\" --notes-file &lt;file&gt; --latest</code></li>\n</ol>\n<h2>8. Restore</h2>\n<p>Always run this, including after a failure or an abandoned release.</p>\n<ol>\n<li>Kill the smoke-test server from §6.</li>\n<li>Restore whatever §0.4 recorded:\n<ul>\n<li>Dev link present before: <code>bun remove -g moi-computer</code>, then <code>bun link</code> — <strong>both run from\nthe repo root</strong>. <code>bun link</code> registers <em>the package in the current directory</em>, so running it\nanywhere else either errors (<code>package.json missing \"name\"</code>) or links the wrong thing. These\nare two separate commands with a directory requirement, not one chained sequence; if your\ntooling resets the working directory between calls, set it explicitly for each.\nVerify <code>~/.bun/install/global/node_modules/moi-computer</code> symlinks back to the repo, and\n<code>moi version</code> reports <code>X.Y.Z (githash)</code>.</li>\n<li>Nothing installed before: <code>bun remove -g moi-computer</code>.</li>\n</ul>\n</li>\n<li><code>rm -f moi-computer-*.tgz</code> and <code>rm -rf dist/</code>. A leftover <code>dist/index.html</code> silently shadows\nthe dev client for linked <code>moi</code> runs (<code>server/static.ts</code>); only <code>bun run dev</code> ignores it.</li>\n<li>If §6 stopped a dev server, say so plainly in the final report and leave restarting it to the\nuser — <code>bun run dev</code> belongs in their terminal, not in a background process you own.</li>\n</ol>\n","files":[{"path":"SKILL.md","sizeBytes":10052,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-08-24T17:04:02.680133Z","sha256":"4FBE63D971057D80C7BCD23CF6433E2B3F64450043C22B6EE62158E5916367D9","sizeBytes":4631},"review":null,"source":{"repositoryUrl":"https://github.com/molefrog/moi","path":".agents/skills/publish-release","license":null,"commit":"be1ed10c3cb856203f325c837d7872acb0b6ddae","subtreeSha":"522B5FA0DB73F7D41A1E97B904E3F5855D011876C3029EA2A977ADDDE995CE64","lastSyncedAt":"2026-09-27T19:34:05.086653Z"},"reviewedAt":"2026-08-24T17:16:09.154575Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/molefrog/moi/tree/main/.agents/skills/publish-release"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install molefrog-moi@llmmart"},{"target":"git","command":"git clone https://github.com/molefrog/moi.git"}]}