Implement unified CI/CD release automation with semantic versioning across all three packages:
- Python (headroom-ai) — pip package on PyPI
- TypeScript SDK (headroom-ai) — npm package on npmjs.org
- OpenClaw plugin (headroom-openclaw) — npm package on npmjs.org and GitHub Package Registry
Currently the three packages are independently versioned (0.5.25 / 0.1.0 / 0.1.0). This PR introduces a single-source-of-truth version in pyproject.toml that propagates to all packages on every release, driven by conventional commit messages.
Fixes #(issue number)
- Bug fix (non-breaking change that fixes an issue)
- New feature (non-breaking change that adds functionality)
- Breaking change (fix or feature that would cause existing functionality to change)
- Documentation update
- Performance improvement
- Code refactoring (no functional changes)
Scripts:
scripts/version-sync.py— Reads version frompyproject.toml, updates all 4 version files. Supports--version X.Y.Zand--bump {major,minor,patch}.scripts/changelog-gen.py— Parses conventional commits since last tag, groups by type, generates markdown changelog with breaking change detection.scripts/verify-versions.py— Pre-release sanity check that all 4 version files are in sync.scripts/tests/test_version_sync.py— 5 tests for version-sync.pyscripts/tests/test_changelog_gen.py— 23 tests for changelog-gen.py
Workflows:
.github/workflows/release.yml— Unified release pipeline: detect → build → publish-pypi → publish-npm → publish-github-packages → create-release.commitlintrc.json— Conventional commit enforcement via@commitlint/config-conventional
Local Testing (act):
.actrc— Defaultactflags (Ubuntu runner, reuse, quiet).github/act/dry-run.json—actevent file for dry-run testing.github/act/push-feat.json—actevent file for simulating a feat commit.actrc.local.example— Local override template foract.env.act.example— Secrets documentation template foractlocal testing
Documentation:
docs/content/docs/releases.mdx— Full documentation for the release pipeline, testing guide, and configuration reference
.github/workflows/ci.yml— Addedcommitlintjob to enforce conventional commits.github/workflows/publish.yml— Changed fromreleasetrigger toworkflow_dispatchonly (superseded byrelease.yml).github/workflows/release.yml— Rewritten with canonical+commit-height algorithm (no more commit loop).gitignore— Added!scripts/version-sync.py,!scripts/changelog-gen.py,!scripts/verify-versions.py,!scripts/tests/,.env.act,.actrc.local
- Unit tests pass (
pytest)scripts/tests/test_version_sync.py— 5/5 passingscripts/tests/test_changelog_gen.py— 23/23 passing
- Linting passes (
ruff check .) - Type checking passes (
mypy headroom) — pre-existing issue inheadroom/cli/wrap.py:487(unrelated) - New tests added for new functionality
- Workflow tested with
act(dry-run passes all jobs through build step — no infinite loop)
The canonical+commit-height algorithm was validated with test cases:
- Canonical
0.5.25, no prior tag,feat:commit → git tagv0.6.0.0, npm0.6.0✅ - Canonical
0.5.25, tagv0.5.25.2,fix:commit → git tagv0.5.25.3, npm0.5.26✅ - Canonical
0.5.25, no prior tag,fix:commit → git tagv0.5.25.0, npm0.5.25✅ - Manual override
1.2.3→ git tagv1.2.3, npm1.2.3✅
scripts/tests/test_version_sync.py .....
scripts/tests/test_changelog_gen.py .......................
- My code follows the project's style guidelines
- I have performed a self-review of my code
- I have commented my code, particularly in hard-to-understand areas
- My changes generate no new warnings
- I have added tests that prove my fix is effective or that my feature works
- New and existing unit tests pass locally with my changes
- I have made corresponding changes to the documentation
- I have updated the CHANGELOG.md if applicable
Canonical + Commit Height Algorithm — The workflow NEVER commits back to the repo. pyproject.toml is the canonical source of truth, updated manually before merging.
| Commit | Bump | Git Tag | npm Version |
|---|---|---|---|
fix:, ci:, chore:, perf:, refactor: |
patch | v0.5.25.3 |
0.5.26 |
feat: |
minor | v0.6.0.0 |
0.6.0 |
feat!: or feat: + BREAKING CHANGE body |
major | v1.0.0.0 |
1.0.0 |
The git tag uses v{canonical}.{height} (e.g., v0.5.25.3 = 3 commits since canonical 0.5.25). npm versions use 3-part semver, bumped from canonical.
| Package | Target | Status |
|---|---|---|
headroom-ai (Python) |
PyPI | ✅ via pypa/gh-action-pypi-publish |
headroom-ai (TypeScript SDK) |
npmjs.org | ✅ via npm publish |
headroom-openclaw |
npmjs.org | ✅ via npm publish |
headroom-openclaw |
GitHub Package Registry | ✅ via npm publish --registry npm.pkg.github.com |
Each publish job requires both dry_run != 'true' and the corresponding skip variable not set:
| Variable | Effect |
|---|---|
PYPI_SKIP=true |
Skip PyPI publish |
NPM_SKIP=true |
Skip both npm publishes |
GH_PACKAGES_SKIP=true |
Skip GitHub Package Registry publish |
Set in: GitHub repo → Settings → Variables → Actions Variables.
- Auto: On push to
main— analyzes latest commit, bumps version, builds, publishes, creates GitHub Release - Manual:
workflow_dispatchwith optionalversionoverride anddry_runflag - Paths ignore: Skips runs when only
docs/,.github/workflows/ci.yml,.github/workflows/publish.yml,scripts/,.commitlintrc.json,.actrc,.github/act/, or.env.act.examplechange
# Install act
winget install act
# Dry-run (no publishes)
act -W .github/workflows/release.yml -e .github/act/dry-run.json
# Test feat: commit (minor bump)
act -W .github/workflows/release.yml -e .github/act/push-feat.json| Secret | Purpose |
|---|---|
NPM_TOKEN |
Publishing to npmjs.org |
GITHUB_TOKEN |
GitHub Package Registry (auto-provided by GitHub Actions) |
PyPI uses trusted publisher OIDC — no secret required, only the pypi GitHub Environment must be configured.
The TypeScript packages are currently at 0.1.0 while Python is at 0.5.25. The first release will align all three to the same version. Update pyproject.toml to the desired canonical version before merging, then use workflow_dispatch with a manual version input to set the target explicitly.
After each release, update pyproject.toml to match the published version to keep the canonical current and ensure unique git tags.
All package names and registries are top-level env constants in release.yml:
env:
PYPI_PACKAGE: headroom-ai
PYPI_ENVIRONMENT: pypi
NPM_REGISTRY_URL: https://registry.npmjs.org
NPM_SDK_PACKAGE: headroom-ai
NPM_OPENCLAW_PACKAGE: headroom-openclaw
GITHUB_PACKAGES_REGISTRY_URL: https://npm.pkg.github.com