How-to for cutting pi-agent-dashboard release. Goal: low-friction, human-curated release notes — no generator tooling; discipline during dev + curation pass at tag time.
flowchart LR
Dev["Development: PR appends bullets to [Unreleased]"] --> Cut["Cut release: Promote [Unreleased], bump + tag"] --> CI["CI publishes: npm + Electron, GitHub Release"]
Single source of truth: CHANGELOG.md. GitHub Release
body extracted automatically from matching section at tag time.
Conventional Commits prefixes, enforced by code review only (no commit lint, no husky hooks).
| Prefix | Meaning |
|---|---|
feat: |
User-visible new capability |
fix: |
Bug fix |
refactor: |
Internal restructure, no behaviour change |
docs: |
Docs-only changes |
test: |
Test-only changes |
chore: |
Dependency bumps, tooling, version bumps |
ci: |
CI / release workflow changes |
Optional scopes in parens encouraged (feat(error-banner): …).
PR ships user-visible behaviour → add bullet under matching subsection
of ## [Unreleased] in CHANGELOG.md:
## [Unreleased]
### Added
- Drop-and-paste screenshots directly into the OpenSpec explore dialog.
### Changed
### Fixed
- Fork entryId timing: leaf registry now resolves the parent message correctly.Bullets in end-user language, not commit-subject shorthand. Link to relevant docs when helpful. Missing bullet does not block PR — release author back-fills during curation.
- Review
git log <last-tag>..HEAD; confirm every user-visible change has bullet under## [Unreleased]. - Add anything contributors missed.
- Tighten wording. Reorder for impact.
- Pick next version per SemVer: feature additions → minor, bug fixes only → patch, breaking changes → major.
In CHANGELOG.md:
-
Rename
## [Unreleased]→## [<version>] - <YYYY-MM-DD>(today, no leadingv). -
Insert fresh empty
## [Unreleased]section above it:## [Unreleased] ### Added ### Changed ### Fixed ## [<version>] - <YYYY-MM-DD> ...
npm version <version> --workspaces --include-workspace-root --no-git-tag-version
node scripts/sync-versions.jsFirst command updates version in package.json + every workspace under
packages/* in single edit. Second (sync-versions.js) rewrites every
inter-package dependency specifier (e.g.
"@blackbelt-technology/pi-dashboard-shared": "^<old>") to bumped
version. Without it, published root declares "pi-dashboard-server": "^<old>" while actual server tarball ^<new> — inconsistent registry
metadata.
Verify: git diff package.json packages/*/package.json — expected:
lockstep version bumps + synchronised inter-package dep specifiers.
Why separate script? npm CLI does not implement
workspace:protocol (pnpm/yarn-only). Use plain semver ranges ("^0.3.0") + sync at bump time. CI also runssync-versions.jsdefensively inpublish.ymlafternpm version, so forgotten local invocation does not corrupt release.
git add CHANGELOG.md package.json package-lock.json packages/*/package.json
git commit -m "chore(release): v<version>"git tag v<version>
git push origin develop
git push origin v<version>Tag push triggers .github/workflows/publish.yml.
On v* tag push, publish.yml:
-
publishjob — publishes five npm packages in one invocation ofnpm publish --workspaces --include-workspace-root --provenance --access public:@blackbelt-technology/pi-agent-dashboard(root metapackage)@blackbelt-technology/pi-dashboard-shared@blackbelt-technology/pi-dashboard-extension@blackbelt-technology/pi-dashboard-server@blackbelt-technology/pi-dashboard-web
Job runs
node scripts/sync-versions.jsbetweennpm version+npm run buildso inter-package dep specifiers match bumped version even if release author forgot local invocation.packages/electronmarked"private": true, auto-skipped bynpm publish --workspaces; ships as native installers via Electron job. -
electronjob (matrix) — builds DMG (macOS arm64), DEB + AppImage (Linux x64 + arm64), NSISSetup.exe+ ZIP (Windows x64 + arm64). Per-user NSIS artifactPI-Dashboard-Setup-<version>-<arch>.exe; portable.exedropped;.zipunchanged. appIdhu.blackbelt.pi-dashboardimmutable after first NSIS release — changing it strands installed users on the old Add/Remove Programs entry. -
github-releasejob —- Extracts
## [<version>]section fromCHANGELOG.md→release-notes.md. - Extraction fails / returns empty → writes one-line fallback body
pointing at
CHANGELOG.md, logs warning. - Calls
softprops/action-gh-release@v2withbody_path: release-notes.md,draft: true, all Electron artifacts attached.
- Extracts
Release lands as draft — nothing published until Publish clicked on GitHub Releases page.
Auto-extracted body rendered incorrectly (missing section, wrong version, truncated bullets) → fix before publishing:
- Open draft release on GitHub.
- Replace body with correct content from
CHANGELOG.md. - Click Publish release.
Worst case (no release at all, wrong artifacts) → delete tag, fix, re-push:
git push --delete origin v<version>
git tag --delete v<version>
# fix the issue, bump if needed
git tag v<version>
git push origin v<version>- Announce in project channels (Discord, X, etc. — if/when exist).
- Monitor GitHub Issues for install/upgrade regressions.
- Leave
## [Unreleased]empty-but-present so next contributor has obvious target.
Production tags vX.Y.Z publish immediately, not draft.
Production releases ship latest.yml, latest-mac.yml, latest-linux.yml.
electron-updater clients resolve /releases/latest.
Stop NEW clients updating to bad release: flip release back to draft.
gh release edit v<version> --draftDraft release excluded from /releases/latest.
electron-updater stops resolving it.
Unpublish via GitHub UI works too.
Flip to draft does NOT recall update already downloaded or applied. Draft only prevents further clients resolving release.
Supersede bad release clients already pulled: cut higher production tag vX.Y.Z+1.
Clients update forward.
Deletion alone does not roll clients back.
Bad latest*.yml shipped (wrong sha512, missing file): re-cut corrected metadata under higher tag.
Do not hand-edit release assets.
sha512 in latest*.yml must match uploaded binary.
Full revoke (delete release + tag + npm deprecate): use release-revoke skill.