From 24b0c05cb6b94cf749b1f3046801e7a985a0a16e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 24 Feb 2026 21:27:49 -0500 Subject: [PATCH] Migrate from changelog.yaml to towncrier fragments Replace the old changelog.yaml workflow with towncrier-based changelog management: - Add changelog.d/ directory with fragment for this change - Add .github/bump_version.py for semver inference from fragments - Add [tool.towncrier] config and towncrier dep to pyproject.toml - Update Makefile changelog target to use towncrier - Add PR workflow with check-changelog job - Remove changelog.yaml Co-Authored-By: Claude Opus 4.6 --- .github/bump_version.py | 74 +++++++++++++++++++++ .github/workflows/pr.yaml | 21 ++++++ Makefile | 3 +- changelog.d/.gitkeep | 0 changelog.d/migrate-to-towncrier.changed.md | 1 + changelog.yaml | 15 ----- pyproject.toml | 34 +++++++++- 7 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 .github/bump_version.py create mode 100644 .github/workflows/pr.yaml create mode 100644 changelog.d/.gitkeep create mode 100644 changelog.d/migrate-to-towncrier.changed.md delete mode 100644 changelog.yaml diff --git a/.github/bump_version.py b/.github/bump_version.py new file mode 100644 index 0000000..add9774 --- /dev/null +++ b/.github/bump_version.py @@ -0,0 +1,74 @@ +"""Infer semver bump from towncrier fragment types and update version.""" + +import re +import sys +from pathlib import Path + + +def get_current_version(pyproject_path: Path) -> str: + text = pyproject_path.read_text() + match = re.search(r'^version\s*=\s*"(\d+\.\d+\.\d+)"', text, re.MULTILINE) + if not match: + print( + "Could not find version in pyproject.toml", + file=sys.stderr, + ) + sys.exit(1) + return match.group(1) + + +def infer_bump(changelog_dir: Path) -> str: + fragments = [ + f + for f in changelog_dir.iterdir() + if f.is_file() and f.name != ".gitkeep" + ] + if not fragments: + print("No changelog fragments found", file=sys.stderr) + sys.exit(1) + categories = {f.suffix.lstrip(".") for f in fragments} + for f in fragments: + parts = f.stem.split(".") + if len(parts) >= 2: + categories.add(parts[-1]) + if "breaking" in categories: + return "major" + if "added" in categories or "removed" in categories: + return "minor" + return "patch" + + +def bump_version(version: str, bump: str) -> str: + major, minor, patch = (int(x) for x in version.split(".")) + if bump == "major": + return f"{major + 1}.0.0" + elif bump == "minor": + return f"{major}.{minor + 1}.0" + else: + return f"{major}.{minor}.{patch + 1}" + + +def update_file(path: Path, old_version: str, new_version: str): + text = path.read_text() + updated = text.replace( + f'version = "{old_version}"', + f'version = "{new_version}"', + ) + if updated != text: + path.write_text(updated) + print(f" Updated {path}") + + +def main(): + root = Path(__file__).resolve().parent.parent + pyproject = root / "pyproject.toml" + changelog_dir = root / "changelog.d" + current = get_current_version(pyproject) + bump = infer_bump(changelog_dir) + new = bump_version(current, bump) + print(f"Version: {current} -> {new} ({bump})") + update_file(pyproject, current, new) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml new file mode 100644 index 0000000..75bd8f3 --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,21 @@ +name: Pull Request CI + +on: + pull_request: + branches: [main, master] + +jobs: + check-changelog: + name: Check changelog fragment + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check for changelog fragment + run: | + FRAGMENTS=$(find changelog.d -type f ! -name '.gitkeep' | wc -l) + if [ "$FRAGMENTS" -eq 0 ]; then + echo "::error::No changelog fragment found in changelog.d/" + echo "Add one with: echo 'Description.' > changelog.d/\$(git branch --show-current)..md" + echo "Types: added, changed, fixed, removed, breaking" + exit 1 + fi diff --git a/Makefile b/Makefile index debf562..141f487 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,8 @@ build: python -m build changelog: - build-changelog changelog.yaml --output CHANGELOG.md --start-from 0.1.0 + python .github/bump_version.py + towncrier build --yes --version $$(python -c "import re; print(re.search(r'version = \"(.+?)\"', open('pyproject.toml').read()).group(1))") clean: find . -type f -name "*.pyc" -delete diff --git a/changelog.d/.gitkeep b/changelog.d/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/changelog.d/migrate-to-towncrier.changed.md b/changelog.d/migrate-to-towncrier.changed.md new file mode 100644 index 0000000..4abe8da --- /dev/null +++ b/changelog.d/migrate-to-towncrier.changed.md @@ -0,0 +1 @@ +Migrate from changelog.yaml to towncrier fragments. diff --git a/changelog.yaml b/changelog.yaml deleted file mode 100644 index 3a5eda5..0000000 --- a/changelog.yaml +++ /dev/null @@ -1,15 +0,0 @@ -- bump: minor - changes: - added: - - Initial PolicyEngine Singapore microsimulation model setup - - Singapore-specific entity definitions (Person, TaxUnit, CPFUnit, BenefitUnit, Household) - - Directory structure for Singapore government agencies (IRAS, CPF, MSF, MOM) - - SGD currency support in model API - - Multi-agent development workflow from PolicyEngine US - - Basic repository structure with Makefile, pyproject.toml, and README - - Test framework setup with pytest configuration - - Parameter structure for income tax, CPF, GST, ComCare, and WorkFare programs - - Variable structure organized by government agency - - Development environment setup with Black formatter and line checking - - CI/CD foundation with comprehensive test coverage framework - date: 2024-08-26 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8c58391..180aacb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ dev = [ "plotly>=5.19.0", "ipykernel>=6.29.5", "ipywidgets>=8.1.5", + "towncrier>=24.0.0", ] [project.urls] @@ -95,4 +96,35 @@ exclude = [ "**/*.pyo", "**/.DS_Store", "**/Thumbs.db", -] \ No newline at end of file +] + +[tool.towncrier] +directory = "changelog.d" +filename = "CHANGELOG.md" +package_dir = "." +title_format = "## {version}" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking changes" +showcontent = true + +[[tool.towncrier.type]] +directory = "added" +name = "Added" +showcontent = true + +[[tool.towncrier.type]] +directory = "changed" +name = "Changed" +showcontent = true + +[[tool.towncrier.type]] +directory = "fixed" +name = "Fixed" +showcontent = true + +[[tool.towncrier.type]] +directory = "removed" +name = "Removed" +showcontent = true \ No newline at end of file