This document describes the complete release process for beads, including GitHub releases, Homebrew, PyPI (MCP server), and npm packages.
- Overview
- Prerequisites
- Release Checklist
- 1. Prepare Release
- 2. GitHub Release
- 3. Homebrew Update
- 4. PyPI Release (MCP Server)
- 5. npm Package Release
- 6. Verify Release
- Prerelease / Release Candidate (RC) Workflow
- Hotfix Releases
- Rollback Procedure
A beads release involves multiple distribution channels:
- GitHub Release - Binary downloads for all platforms
- Homebrew - macOS/Linux package manager
- PyPI - Python MCP server (
beads-mcp) - npm - Node.js package for Claude Code for Web (
@beads/bd)
For routine releases, use the fully automated release script:
./scripts/release.sh 0.22.0This handles version bump, tests, git tag, Homebrew update, and local
installation in one shot. See scripts/README.md
for details. The rest of this document is the manual / step-by-step process,
useful for understanding what release.sh does and for handling edge cases
(hotfixes, rollbacks, manual PyPI/npm publishes).
gitwith push access to gastownhall/beadsgoreleaserfor building binariesnpmwith authentication (for npm releases)python3andtwine(for PyPI releases)ghCLI (GitHub CLI, optional but recommended)
- GitHub: Write access to repository and ability to create releases
- GitHub: Ability to create protected
v*release tags. The repository should restrictrefs/tags/v*creation, updates, and deletion to trusted release maintainers. - PyPI: Maintainer access to
beads-mcppackage - npm: Member of
@beadsorganization
# Check git
git remote -v # Should show gastownhall/beads
# Check goreleaser
goreleaser --version
# Check GitHub CLI (optional)
gh auth status
# Check npm
npm whoami # Should show your npm username
# Check Python/twine (for MCP releases)
python3 --version
twine --versionBefore starting a release:
- All tests passing (
go test ./...) - npm package tests passing (
cd npm-package && npm run test:all) - Upgrade smoke tests pass (
make test-upgrade) — see Release Stability Gate - Regression tests pass (
make test-regression) - CHANGELOG.md updated with release notes (see format below)
- Breaking changes documented with migration steps and recovery instructions
- No uncommitted changes
- On
mainbranch and up to date with origin
IMPORTANT: Do this FIRST before running bump-version script.
Add release notes to CHANGELOG.md:
## [0.22.0] - 2025-11-04
### Added
- New feature X
- New command Y
### Changed
- Improved performance of Z
### Fixed
- Bug in component A
### Breaking Changes
- Changed behavior of B (migration guide)Commit the CHANGELOG changes:
git add CHANGELOG.md
git commit -m "docs: Add CHANGELOG entry for v0.22.0"
git push origin mainUse the version bump script to update all version references and create the release tag:
# Dry run - shows what will change
./scripts/bump-version.sh 0.22.0
# Full release with all local installations
./scripts/bump-version.sh 0.22.0 --commit --tag --push --allAvailable flags:
| Flag | Description |
|---|---|
--commit |
Create a git commit with version changes |
--tag |
Create annotated git tag (requires --commit) |
--push |
Push commit and tag to origin (requires --tag) |
--install |
Build and install bd to ~/go/bin AND ~/.local/bin |
--mcp-local |
Install beads-mcp from local source via uv/pip |
--upgrade-mcp |
Upgrade beads-mcp from PyPI (after PyPI publish) |
--restart-servers |
Restart all Dolt servers to pick up new version |
--all |
Shorthand for --install --mcp-local --restart-servers |
This updates:
cmd/bd/version.go- CLI version constantintegrations/beads-mcp/pyproject.toml- MCP server versionintegrations/beads-mcp/src/beads_mcp/__init__.py- MCP Python versionplugins/beads/.claude-plugin/plugin.json- Claude plugin versionplugins/beads/.codex-plugin/plugin.json- Codex plugin version.claude-plugin/marketplace.json- Claude marketplace versionnpm-package/package.json- npm package versioncmd/bd/templates/hooks/*- Git hook versionsREADME.md- Documentation versionPLUGIN.md- Version requirementsCHANGELOG.md- Creates release entry from [Unreleased]
The --commit --tag --push flags will:
- Create a git commit with all version changes
- Create an annotated tag
v0.22.0 - Push both commit and tag to origin
This triggers GitHub Actions to build release artifacts automatically.
The tag workflow re-runs release-critical package gates before publishing:
make ci-package-mcpbuilds and validates the MCP package, then the PyPI job publishes the validateddist/*artifact from that gate.make ci-package-npmvalidates the npm wrapper package before npm publish. publishes GitHub release assets.
The npm publish job also waits for the macOS release assets, because the npm
postinstall script downloads platform-specific archives from the GitHub
release.
Recommended workflow:
# 1. Update CHANGELOG.md and cmd/bd/info.go with release notes (manual step)
# 2. Bump version and install everything locally
./scripts/bump-version.sh 0.22.0 --commit --all
# 3. Test locally, then tag and push
git tag -a v0.22.0 -m "Release v0.22.0"
git push origin main
git push origin v0.22.0The release workflow is intentionally gated to refs/tags/v*. A manual
workflow dispatch from a branch will skip publishing jobs; manual reruns must
select the release tag.
Alternative (step-by-step):
# Just commit
./scripts/bump-version.sh 0.22.0 --commit
# Then manually tag and push
git tag -a v0.22.0 -m "Release v0.22.0"
git push origin main
git push origin v0.22.0GoReleaser automates binary building and GitHub release creation:
# Clean any previous builds
rm -rf dist/
# Create release (requires GITHUB_TOKEN)
export GITHUB_TOKEN="your-github-token"
goreleaser release --clean
# Or use gh CLI for token
gh auth token | goreleaser release --cleanThis will:
- Build binaries for all platforms (macOS, Linux, Windows - amd64/arm64)
- Create checksums
- Generate release notes from CHANGELOG.md
- Upload everything to GitHub releases
- Mark as latest release
If goreleaser doesn't work:
# Build for all platforms
./scripts/build-all-platforms.sh
# Create GitHub release
gh release create v0.22.0 \
--title "v0.22.0" \
--notes-file CHANGELOG.md \
dist/*.tar.gz \
dist/*.zip \
dist/checksums.txt- Visit https://github.com/gastownhall/beads/releases
- Verify v0.22.0 is marked as "Latest"
- Check all platform binaries are present:
beads_0.22.0_darwin_amd64.tar.gzbeads_0.22.0_darwin_arm64.tar.gzbeads_0.22.0_linux_amd64.tar.gzbeads_0.22.0_linux_arm64.tar.gzbeads_0.22.0_windows_amd64.zipchecksums.txt
Homebrew uses the beads formula in homebrew-core. Do not publish or revive
the old bd formula in gastownhall/homebrew-beads; having two independently
updated Homebrew formulas causes version drift and installs the wrong binary for
some users.
Updates to the supported Homebrew formula are handled through Homebrew core after GitHub Release artifacts are available.
After the GitHub Release is published, verify the Homebrew package:
# Update Homebrew
brew update
# Install/upgrade
brew upgrade beads # or: brew install beads
# Verify
bd version # Should show 0.22.0The MCP server is a Python package published separately to PyPI.
# Install build tools
pip install build twine
# Verify PyPI credentials
cat ~/.pypirc # Should have token or credentials# Navigate to MCP server directory
cd integrations/mcp/server
# Verify version was updated
cat pyproject.toml | grep version
# Clean old builds
rm -rf dist/ build/ *.egg-info
# Build package
python -m build
# Verify contents
tar -tzf dist/beads-mcp-0.22.0.tar.gz
# Upload to PyPI (test first)
twine upload --repository testpypi dist/*
# Verify on test PyPI
pip install --index-url https://test.pypi.org/simple/ beads-mcp==0.22.0
# Upload to production PyPI
twine upload dist/*# Check package page
open https://pypi.org/project/beads-mcp/
# Install and test
pip install beads-mcp==0.22.0
python -m beads_mcp --versionUpdate the plugin marketplace metadata files:
# Update .claude-plugin/marketplace.json
# Change version to match current release
vim .claude-plugin/marketplace.json
# Update plugins/beads/.claude-plugin/plugin.json if needed
vim plugins/beads/.claude-plugin/plugin.json
# Update plugins/beads/.codex-plugin/plugin.json if needed
vim plugins/beads/.codex-plugin/plugin.json
# Commit changes
git add .claude-plugin/ plugins/beads/.claude-plugin/ plugins/beads/.codex-plugin/
git commit -m "chore: Update plugin marketplaces to v0.22.0"Note: These files define how beads appears in Claude Code and Codex plugin marketplaces. Version should match the release version.
The published docs are the Mintlify site rooted at docs/, deployed from
main via the Mintlify GitHub integration — no release-time docs snapshot is
needed. The site documents the current release line only (see
engdocs/decisions/2026-07-10-mintlify-docs-overhaul.md). Day to day, the
generated CLI reference is kept fresh by scripts/generate-cli-docs.sh and
its PR drift gate, not by the release process.
But the release pin IS a release-time step. docs/cli-docs.pin names
the release tag the docs corpus is generated from and validated against
(engdocs/decisions/2026-07-17-docs-release-pin.md); it lags main between
releases by design, so any command or flag added on main since the last
bump is invisible to scripts/check-doc-flags.sh Check 4 ("covers all live
top-level CLI commands" passes vacuously for it, e.g. wy-gx5rj for bd sync). Bump it as part of THIS release, not a follow-up:
# After tagging (see "Update Version and Create Release Tag" above):
echo "v0.22.0" > docs/cli-docs.pin
./scripts/generate-cli-docs.sh
git add docs/cli-docs.pin docs/CLI_REFERENCE.md docs/cli-reference docs/docs.json
git commit -m "docs: bump CLI docs pin to v0.22.0"
git push origin mainSkipping this step doesn't fail fast — Check 4 stays green (it validates against the old pin) until the next bump, at which point every command added across the skipped releases shows up at once as a pile of "missing from docs/CLI_REFERENCE.md" failures with no obvious release to blame.
The npm package wraps the native binary for Node.js environments.
# Verify npm authentication
npm whoami # Should show your username
# Verify you're in @beads org
npm org ls beads# Navigate to npm package
cd npm-package
# Version should already be updated by bump-version.sh
cat package.json | grep version
# Run all tests
npm run test:all
# Should see:
# ✅ All unit tests passed
# ✅ All integration tests passed# Pack the package
npm pack
# Install globally from tarball
npm install -g ./beads-bd-0.22.0.tgz
# Verify binary downloads correctly
bd version # Should show 0.22.0
# Test in a project
mkdir /tmp/test-npm-bd
cd /tmp/test-npm-bd
git init
bd init
bd create "Test issue" -p 1
bd list
# Cleanup
npm uninstall -g @beads/bd
rm -rf /tmp/test-npm-bd
cd -
rm beads-bd-0.22.0.tgz# IMPORTANT: Ensure GitHub release with binaries is live first!
# The postinstall script downloads from GitHub releases
# Publish to npm (first time use --access public)
npm publish --access public
# Or for subsequent releases
npm publish# Check package page
open https://www.npmjs.com/package/@beads/bd
# Install and test
npm install -g @beads/bd
bd version # Should show 0.22.0
# Test postinstall downloaded correct binary
which bd
bd --helpAfter all distribution channels are updated, verify each one:
# Download and test binary
wget https://github.com/gastownhall/beads/releases/download/v0.22.0/beads_0.22.0_darwin_arm64.tar.gz
tar -xzf beads_0.22.0_darwin_arm64.tar.gz
./bd versionbrew update
brew upgrade beads
bd versionpip install --upgrade beads-mcp
python -m beads_mcp --versionnpm install -g @beads/bd
bd version# Test quick install script
curl -fsSL https://raw.githubusercontent.com/gastownhall/beads/main/scripts/install.sh | bash
bd versioncat docs/cli-docs.pin # should be the tag just released, not an older oneIf it's stale, do the bump from the "Documentation Site (Mintlify)" step above before calling the release done.
Release candidates let a build be validated through the full release pipeline
without promoting it to the stable channels. An RC carries a SemVer prerelease
identifier (e.g. 1.1.0-rc.1); Python tooling normalizes this to PEP 440 form
(1.1.0rc1).
How a prerelease tag differs from a stable release:
- GitHub release is published and marked as a prerelease (goreleaser
release.prerelease: auto), with binaries for all platforms. - Homebrew is not updated (goreleaser
brews.skip_upload: true; the core formula only tracks stable releases). - PyPI and npm publish jobs are skipped. The
publish-pypiandpublish-npmjobs are gated with!contains(github.ref_name, '-'), so a tag containing a-never reaches the stable package channels. - Docs are unaffected. The docs site publishes from
mainvia the Mintlify GitHub integration; there is no release-time docs snapshot for either prereleases or stable releases.
# 1. Update CHANGELOG.md and cmd/bd/info.go with the RC notes (manual step),
# same as a stable release. Date the CHANGELOG section.
# 2. Bump versions. update-versions.sh accepts a prerelease identifier and,
./scripts/update-versions.sh 1.1.0-rc.1
# Windows PE numeric fields (winres file_version/product_version and the
# manifest <assemblyIdentity> version) are set to the base version 1.1.0,
# because PE versions must be purely numeric; gen-winres.sh strips the
# prerelease suffix the same way at build time.
# 3. Keep the MCP lockfile in sync (PEP 440 normalizes to 1.1.0rc1), or the
# Package Gate (MCP) check goes red:
(cd integrations/beads-mcp && uv lock)
# 4. Validate locally.
./scripts/check-versions.sh
# 5. Open a PR for the RC prep and have it reviewed. RC prep should land
# through normal review, not auto-merge.After the RC prep is merged to main, cut the tag from the merge commit:
git checkout main && git pull
git tag -a v1.1.0-rc.1 -m "Release candidate v1.1.0-rc.1"
git push origin v1.1.0-rc.1Pushing the v* tag triggers the release workflow with the prerelease behavior
above. Tag creation is restricted to release maintainers; see
Prerequisites.
- Install the RC from the GitHub prerelease assets and exercise the changes it is gating before promoting.
- To promote to stable, bump to the base version with no suffix
(
./scripts/update-versions.sh 1.1.0). The stable release does publish to Homebrew/PyPI/npm, so follow the standard Prepare Release steps from there.
For urgent bug fixes:
# Create hotfix branch from tag
git checkout -b hotfix/v0.22.1 v0.22.0
# Make fixes
# ... edit files ...
# Bump version to 0.22.1
./scripts/bump-version.sh 0.22.1 --commit
# Tag and release
git tag -a v0.22.1 -m "Hotfix release v0.22.1"
git push origin hotfix/v0.22.1
git push origin v0.22.1
# Follow normal release process
goreleaser release --clean
# Merge back to main
git checkout main
git merge hotfix/v0.22.1
git push origin mainIf a release has critical issues:
gh release edit v0.22.0 --prereleaseFollow hotfix procedure above to release 0.22.1.
npm deprecate @beads/bd@0.22.0 "Critical bug, please upgrade to 0.22.1"# Can't delete, but can yank (hide from pip install)
# Contact PyPI support or use web interfaceCreate .github/workflows/release.yml:
name: Release
on:
push:
tags:
- 'v*'
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
- uses: goreleaser/goreleaser-action@v4
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
npm:
needs: goreleaser
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- run: cd npm-package && npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
pypi:
needs: goreleaser
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- run: |
cd integrations/mcp/server
pip install build twine
python -m build
twine upload dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}After a successful release:
-
Upgrade local beads-mcp installation to the new version:
# Option 1: Use the bump-version.sh script (recommended during version bump) ./scripts/bump-version.sh <version> --upgrade-mcp # Option 2: Manual upgrade via pip (if installed globally) pip install --upgrade beads-mcp # Option 3: Manual upgrade via uv tool (if installed as a tool) uv tool upgrade beads-mcp # Verify the new version pip show beads-mcp | grep Version # Restart Claude Code or MCP session to pick up the new version # The MCP server will load the newly installed version
Note: The
--upgrade-mcpflag can be combined with other flags:# Update versions, commit, install bd binary, and upgrade beads-mcp all at once ./scripts/bump-version.sh 0.24.3 --commit --install --upgrade-mcp -
Verify the upgraded CLI:
bd version bd doctor quick
-
Announce on relevant channels (Twitter, blog, etc.)
-
Update documentation if needed
-
Close milestone on GitHub if using milestones
-
Update project board if using project management
-
Monitor for issues in the first 24-48 hours
# Delete tag locally and remotely
git tag -d v0.22.0
git push origin :refs/tags/v0.22.0
# Recreate
git tag -a v0.22.0 -m "Release v0.22.0"
git push origin v0.22.0# Version already published, bump version
npm version patch
npm publish# Ensure GitHub release is published first
# Check binary URL is correct
# Verify version matches in package.json and GitHub release# Check .goreleaser.yml syntax
goreleaser check
# Test build locally
goreleaser build --snapshot --cleanBeads follows Semantic Versioning:
- MAJOR (x.0.0): Breaking changes
- MINOR (0.x.0): New features, backwards compatible
- PATCH (0.0.x): Bug fixes, backwards compatible
Examples:
0.21.5→0.22.0: New features (minor bump)0.22.0→0.22.1: Bug fix (patch bump)0.22.1→1.0.0: Stable release (major bump)
Prereleases: append a SemVer prerelease identifier for release candidates,
e.g. 1.1.0-rc.1. Prerelease tags publish a GitHub prerelease only and stay
off the stable Homebrew/PyPI/npm channels — see
Prerelease / Release Candidate (RC) Workflow.
- Minor releases: Every 2-4 weeks (new features)
- Patch releases: As needed (bug fixes)
- Major releases: When breaking changes are necessary
- Open an issue: https://github.com/gastownhall/beads/issues
- Check existing releases: https://github.com/gastownhall/beads/releases