Files
cimtechniques-service-suite/scripts/release_notes.py
Andy 63169a7644 feat: add versioning and changelog system
Conventional-Commit-driven version bumps (scripts/bump.py), a commit-msg validation hook, and a /release-notes workflow that produces a human-reviewed CHANGELOG.md. Adds an in-app version badge + 'What's new' dialog on the launcher. The version is single-sourced from pyproject.toml (cim_suite.__version__), and the deterministic bump backbone lives in scripts/release/ with tests in tests/release/. Marks the 1.0.0 baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 11:31:22 -04:00

85 lines
3.1 KiB
Python

#!/usr/bin/env python
"""Collect commits since the last release and emit a digest for changelog drafting.
This produces the STRUCTURED INPUT for the human + Claude changelog step. It never
writes CHANGELOG.md and never invents prose — it just buckets commits into
user-visible features/fixes/breaking changes vs. collapsible internal work, using the
same range logic as the bump script. The plain-language summary is drafted by Claude
from this digest and reviewed before it lands (see ``.claude/commands/release-notes.md``).
python scripts/release_notes.py # human-readable digest
python scripts/release_notes.py --json # machine-readable digest (for Claude)
python scripts/release_notes.py --baseline <rev>
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from release import repo # noqa: E402
from release.commits import ParsedCommit, build_digest # noqa: E402
def _commit_dict(c: ParsedCommit) -> dict[str, object]:
return {
"sha": c.sha[:8],
"type": c.type,
"scope": c.scope,
"breaking": c.breaking,
"description": c.description,
}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--json", action="store_true", help="emit JSON for tooling/Claude")
parser.add_argument("--baseline", help="range start commit/tag when no version tag exists")
args = parser.parse_args(argv)
plan = repo.gather(baseline=args.baseline)
digest = build_digest(plan.commits)
bump = plan.detected_bump or "patch"
suggested = plan.current.bump(bump)
if args.json:
payload = {
"current_version": str(plan.current),
"suggested_version": str(suggested),
"suggested_bump": bump,
"detected_bump": plan.detected_bump,
"range": plan.range_spec,
"features": [_commit_dict(c) for c in digest.features],
"fixes": [_commit_dict(c) for c in digest.fixes],
"breaking": [_commit_dict(c) for c in digest.breaking],
"internal_count": len(digest.internal),
"internal_types": sorted({c.type or "other" for c in digest.internal}),
}
print(json.dumps(payload, indent=2))
return 0
rng = plan.range_spec or "(all commits)"
print(f"# Release digest ({plan.current} -> {suggested}, {bump})")
print(f"Range: {rng} Commits: {len(plan.commits)}\n")
for title, items in (
("Breaking changes", digest.breaking),
("Features (feat)", digest.features),
("Fixes (fix)", digest.fixes),
):
print(f"## {title} ({len(items)})")
for c in items:
scope = f"({c.scope})" if c.scope else ""
print(f" - {c.sha[:8]} {c.type}{scope}: {c.description}")
print()
types = sorted({c.type or "other" for c in digest.internal})
print(f"## Internal (collapse to one line): {len(digest.internal)} commit(s) {types}")
return 0
if __name__ == "__main__":
raise SystemExit(main())