Here’s the straight answer to the query you probably pasted into your browser: yes, you can automate the entire OpenClaw npm package publishing cycle — version bump, changelog, tests, npm publish, GitHub release, and even Slack/Discord notifications — without giving CI a free pass to ship broken builds. Below is the exact wiring I use on three production packages (OpenClaw gateway v0.34.1, the daemon, and the browser tool-launcher). It has survived flaky tests, transient npm outages, and one awkward semver accident I’ll show you how to avoid.
Why automate the OpenClaw npm publish pipeline?
Manual publishing gets painful fast when:
- You maintain multiple packages (gateway, daemon, integrations).
- Every pull request potentially ships user-visible changes.
- Engineers cut releases outside your time zone.
The hidden cost is context switching. GitHub reported that jumping between IDE, terminal, GitHub Releases UI, and Slack burns ~10 min per release. Multiplied by weekly patches, that’s a sprint. Automation removes that tax, but only if it’s safer than humans. So we build guardrails first, then the conveyor belt.
Anatomy of a safe npm publishing workflow
I break the pipeline into six stages. Each fails hard, bails out, and never publishes half-baked artifacts.
- Checkout & install — pull repo, cache
node_modules. - Static checks —
typescript --noEmit,eslint,prettier --check. - Unit + integration tests — junit output for easy GH summary.
- Version analysis — decide major/minor/patch via commit messages.
- Publish —
npm publish --provenance --access public. - Release & notify — GitHub Release notes, changelog bump, Slack ping.
The brain behind steps 4-6 is semantic-release v23.0.2. It reads commit messages, computes the next semver, updates package.json, generates notes, tags the commit, and runs npm publish. But raw semantic-release assumes you want to ship every main-branch commit. For OpenClaw we stick a test matrix in front so failures short-circuit.
Setting up semantic-release for OpenClaw
1. Add dev dependencies
npm i -D semantic-release@23.0.2 @semantic-release/changelog @semantic-release/git @semantic-release/npm
2. Commit message convention
We use Conventional Commits. Anything without a conventional header never bumps the version. Quick refresher:
fix(cache): handle Redis disconnect→ patchfeat(shell): add fish shell support→ minorfeat!: drop Node 20 support→ major
3. release.config.js
module.exports = {
branches: [
{ name: 'main' },
{ name: 'next', prerelease: true }
],
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
['@semantic-release/changelog', {
changelogFile: 'CHANGELOG.md'
}],
['@semantic-release/npm', {
pkgRoot: '.',
npmPublish: true
}],
['@semantic-release/git', {
assets: ['package.json', 'CHANGELOG.md'],
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}'
}]
]
};
Two notable tweaks:
- We publish from the repository root. Some monorepos pin a
pkgRoot; adjust if you’re usingnpm workspaces. - The Git plugin commits the bumped version back to
main, so your GitHub view always shows the released number.
Wiring GitHub Actions: tests first, publish later
1. Create .github/workflows/publish.yml
name: CI & Publish
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write # for release notes
packages: write # for npm provenance
id-token: write # for OIDC npm auth
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [22, 21]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test -- --runInBand
publish:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://registry.npmjs.org/@openclaw/gateway
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
- run: npm ci --ignore-scripts
- run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
Why two jobs? publish depends on test. If any Node version fails, publish never runs. That one-liner guard saves your weekend.
We also use OIDC (“tokenless npm upload”). actions/setup-node@v4 now supports it behind NODE_AUTH_TOKEN. I still keep a fallback NPM_PUBLISH_TOKEN secret because npm federation glitches once a month.
Generating and verifying the changelog
Semantic-release writes CHANGELOG.md, but you should sanity-check it. My rule: no release goes out if the changelog is empty or only lists CI chores.
Pre-publish script
"scripts": {
"prepublishOnly": "node scripts/check-changelog.js"
}
// scripts/check-changelog.js
import fs from 'node:fs';
const log = fs.readFileSync('CHANGELOG.md', 'utf8');
if (/###?\s*Bug Fixes|###?\s*Features/.test(log) === false) {
console.error('CHANGELOG has no user-visible entries; aborting publish');
process.exit(1);
}
Because semantic-release runs after tests, this extra gate only trips on human-generated prereleases. It saved me from shipping a “chore:” only version bump once.
Automating GitHub Releases and notifications
Semantic-release already drafts the GitHub Release. You can bolt notifications on top. I push to Slack and Telegram using a tiny composite action:
- name: Notify
if: success() && steps.semantic.outputs.new-release-published == 'true'
uses: ./.github/actions/notify
with:
tag: ${{ steps.semantic.outputs.new-release-version }}
.github/actions/notify/action.yml
name: "Notify messaging channels"
inputs:
tag:
required: true
runs:
using: "composite"
steps:
- run: |
curl -X POST -H 'Content-Type: application/json' \
-d "{\"text\": \"OpenClaw Gateway ${{ inputs.tag }} released\"}" $SLACK_WEBHOOK
curl -X POST https://api.telegram.org/bot$TG_TOKEN/sendMessage \
-d chat_id=$TG_CHAT -d text="OpenClaw Gateway ${{ inputs.tag }} released"
shell: bash
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
TG_TOKEN: ${{ secrets.TG_TOKEN }}
TG_CHAT: ${{ secrets.TG_CHAT }}
Keep the composite action inside the repo so code reviewers can audit it. Supply secrets at the environment level so forks don’t leak them.
Guardrails, rollbacks, and common pitfalls
1. Two-factor auth on npm
npm enforces 2FA for high-impact packages. If you skip OIDC you’ll need --otp. Easiest fix: enable “automation” mode on your npm account, then generate a token flagged for automation (OTP banned). Store it as NPM_PUBLISH_TOKEN.
2. Accidental latest
Pre-releases (1.2.0-next.3) should never become latest. Add prerelease: true to the branch config (see earlier). Semantic-release then tags it @next, not @latest.
3. Yanking a bad version
npm unpublish is locked after 72 h for popular packages (OpenClaw passed the threshold months ago). Instead:
- Deprecate the version:
npm deprecate @openclaw/gateway@1.4.2 "Broken shell exec on Windows". - Patch-release a fixed
1.4.3.
Consumers on caret (^1.4.0) auto-upgrade.
4. Frozen main branch
If you enable required status checks on main but forget to add “semantic-release” to the allowed bypass list, the release commit pushed by the Git plugin will fail the policy and the workflow loops. Add the bot account to your CODEOWNERS or relax the rule for that path.
Putting it all together: end-to-end example
Below is an abbreviated session from last week’s feat(shell): add fish support merge. Timestamps trimmed.
$ git merge --no-ff feature/fish-shell
# CI kicked in
✔ Node 22 lint, tests (1m23s)
✔ Node 21 lint, tests (1m15s)
⇢ semantic-release determines 0.33.0 → 0.34.0 (minor)
⇢ CHANGELOG.md appended
⇢ git tag v0.34.0
⇢ npm publish --provenance
⇢ GitHub Release created https://github.com/openclaw/gateway/releases/tag/v0.34.0
⇢ Slack #openclaw-dev "Gateway 0.34.0 released: fish shell support"
Total human involvement: one PR review. Total elapsed time: 3 min 12 s.
Next step: fork the config, change the package name, and push a dummy feat(test): initial automation commit. If the workflow tags v0.1.0 and your chat lights up, you’ve joined the “never publish manually again” club.