From 7e1b3c41f28f269c650c61d1a142070f654e6de0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 24 Feb 2026 21:28:25 -0500 Subject: [PATCH] Migrate from changelog_entry.yaml to towncrier fragments Replace yaml-changelog with towncrier for changelog management to eliminate merge conflicts on PRs. Co-Authored-By: Claude Opus 4.6 --- .github/bump_version.py | 79 +++++++++++++++++ .github/check-changelog-entry.sh | 7 -- .github/workflows/pr_changelog.yaml | 29 +++---- .../workflows/reusable_changelog_check.yaml | 45 ---------- .github/workflows/versioning.yaml | 17 ++-- Makefile | 7 +- changelog.d/.gitkeep | 0 changelog.d/migrate-to-towncrier.changed.md | 1 + changelog.yaml | 84 ------------------- pyproject.toml | 34 ++++++++ 10 files changed, 135 insertions(+), 168 deletions(-) create mode 100644 .github/bump_version.py delete mode 100755 .github/check-changelog-entry.sh delete mode 100644 .github/workflows/reusable_changelog_check.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..bb0fd6d --- /dev/null +++ b/.github/bump_version.py @@ -0,0 +1,79 @@ +"""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/check-changelog-entry.sh b/.github/check-changelog-entry.sh deleted file mode 100755 index 0d19f7a..0000000 --- a/.github/check-changelog-entry.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -# Fails if changelog_entry.yaml is empty or contains only whitespace -if [ ! -s changelog_entry.yaml ] || ! grep -q '[^[:space:]]' changelog_entry.yaml; then - echo "changelog_entry.yaml is empty. Please add a changelog entry before merging." - exit 1 -fi \ No newline at end of file diff --git a/.github/workflows/pr_changelog.yaml b/.github/workflows/pr_changelog.yaml index 7e5ffac..49ac82a 100644 --- a/.github/workflows/pr_changelog.yaml +++ b/.github/workflows/pr_changelog.yaml @@ -1,30 +1,21 @@ name: Changelog entry + on: pull_request: branches: [main] jobs: - check-fork: + check-changelog: + name: Check changelog fragment runs-on: ubuntu-latest steps: - - name: Check if PR is from fork + - uses: actions/checkout@v4 + - name: Check for changelog fragment run: | - if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then - echo "❌ ERROR: This PR is from a fork repository." - echo "PRs must be created from branches in the main PolicyEngine/L0 repository." - echo "Please close this PR and create a new one following these steps:" - echo "1. git checkout main" - echo "2. git pull upstream main" - echo "3. git checkout -b your-branch-name" - echo "4. git push -u upstream your-branch-name" - echo "5. Create PR from the upstream branch" + 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 - echo "✅ PR is from the correct repository" - - require-entry: - needs: check-fork - uses: ./.github/workflows/reusable_changelog_check.yaml - with: - require_entry: true - validate_format: true \ No newline at end of file diff --git a/.github/workflows/reusable_changelog_check.yaml b/.github/workflows/reusable_changelog_check.yaml deleted file mode 100644 index 32aa538..0000000 --- a/.github/workflows/reusable_changelog_check.yaml +++ /dev/null @@ -1,45 +0,0 @@ -name: Reusable Changelog Check - -on: - workflow_call: - inputs: - require_entry: - description: 'Whether to require a changelog entry exists' - required: false - default: true - type: boolean - validate_format: - description: 'Whether to validate the changelog format' - required: false - default: true - type: boolean - -jobs: - changelog-check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Ensure changelog entry exists - if: inputs.require_entry - run: .github/check-changelog-entry.sh - - - name: Setup Python - if: inputs.validate_format - uses: actions/setup-python@v5 - with: - python-version: 3.13 - - - name: Install yaml-changelog - if: inputs.validate_format - run: pip install yaml-changelog - - - name: Validate changelog format - if: inputs.validate_format - run: | - # Test if changelog entry is valid by trying to build it - if [ -f changelog_entry.yaml ]; then - build-changelog changelog.yaml --output /tmp/test_changelog.yaml --append-file changelog_entry.yaml --start-from 0.1.0 - fi \ No newline at end of file diff --git a/.github/workflows/versioning.yaml b/.github/workflows/versioning.yaml index 424cb9c..212b532 100644 --- a/.github/workflows/versioning.yaml +++ b/.github/workflows/versioning.yaml @@ -7,7 +7,7 @@ on: - main paths: - - changelog_entry.yaml + - "changelog.d/**" - "!pyproject.toml" jobs: @@ -18,19 +18,20 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.ref }} token: ${{ secrets.POLICYENGINE_GITHUB }} + fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v5 with: python-version: 3.13 - - name: Build changelog - run: pip install yaml-changelog && make changelog - - name: Preview changelog update - run: ".github/get-changelog-diff.sh" + - name: Install towncrier + run: pip install towncrier + - name: Bump version and build changelog + run: | + python .github/bump_version.py + towncrier build --yes --version $(python -c "import re; print(re.search(r'version = \"(.+?)\"', open('pyproject.toml').read()).group(1))") - name: Update changelog uses: EndBug/add-and-commit@v9 with: add: "." - message: Update package version \ No newline at end of file + message: Update package version diff --git a/Makefile b/Makefile index 756ed4a..86835d7 100644 --- a/Makefile +++ b/Makefile @@ -32,11 +32,8 @@ type-check: mypy l0 --ignore-missing-imports changelog: - build-changelog changelog.yaml --output changelog.yaml --update-last-date --start-from 0.1.0 --append-file changelog_entry.yaml - build-changelog changelog.yaml --org PolicyEngine --repo L0 --output CHANGELOG.md --template .github/changelog_template.md - bump-version changelog.yaml pyproject.toml - rm changelog_entry.yaml || true - touch changelog_entry.yaml + 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: rm -rf build/ 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..865484a --- /dev/null +++ b/changelog.d/migrate-to-towncrier.changed.md @@ -0,0 +1 @@ +Migrated from changelog_entry.yaml to towncrier fragments to eliminate merge conflicts. diff --git a/changelog.yaml b/changelog.yaml deleted file mode 100644 index 72914e1..0000000 --- a/changelog.yaml +++ /dev/null @@ -1,84 +0,0 @@ -- changes: - added: - - Initial release of L0 regularization package - - HardConcrete distribution implementation - - L0Linear, L0Conv2d, L0DepthwiseConv2d layers - - SparseMLP for structured sparsity - - L0Gate, SampleGate, FeatureGate, HybridGate for selection tasks - - L0, L2, and combined L0L2 penalty computation - - Temperature scheduling for training stability - - Comprehensive test suite - - GitHub Actions CI/CD pipeline - date: 2025-01-01 00:00:00 - version: 0.1.0 -- bump: patch - changes: - fixed: - - 'CI/CD: Separate PR and push workflows' - date: 2025-01-02 00:00:00 -- bump: patch - changes: - added: - - Sparse calibration stress test example - date: 2025-01-03 00:00:00 -- bump: patch - changes: - added: - - PolicyEngine workflow best practices integration - date: 2025-08-26 22:20:57 -- bump: minor - changes: - added: - - Positive weight constraints for calibration module - - PolicyEngine workflow best practices integration - date: 2025-08-31 13:00:17 -- bump: minor - changes: - added: - - Group-wise loss averaging for calibration to balance contributions from targets - with different cardinalities - - Improved training output with meaningful error percentages and sparsity statistics - changed: - - Simplified active weight detection in SparseCalibrationWeights (removed threshold - parameter) - - Enhanced verbose output during calibration training to show relative errors - and sparsity percentage - date: 2025-09-05 13:20:21 -- bump: patch - changes: - removed: - - Removed mypy type checking from CI/CD workflow to eliminate type annotation - overhead - date: 2025-09-05 13:33:54 -- bump: minor - changes: - added: - - Jitter parameter for improved numerical stability in calibration layers - - More flexible parameter initialization options for calibration - - Python 3.11 support (previously required 3.13) - changed: - - Enhanced argument handling in calibration module - - Improved parameter initialization defaults - fixed: - - Numerical stability issues in edge cases during calibration - date: 2025-09-10 03:01:41 -- bump: patch - changes: - fixed: - - Fixed CI build failure by adding jupyter-book to docs dependencies - date: 2025-09-10 03:17:25 -- bump: minor - changes: - added: - - Weight distribution diagnostics during training showing buckets (<0.01, 0.01-0.1, - 0.1-1, 1-10, 10-1000, >1000) - - use_gates parameter to optionally disable L0 gates in SparseCalibrationWeights - date: 2026-01-21 03:21:38 -- bump: minor - changes: - added: - - Per-group loss multipliers via group_multipliers parameter in SparseCalibrationWeights.fit() - - normalize_groups parameter to control within-group normalization independently - of target_groups - - Verbose echo of group weighting config at start of fit() - date: 2026-02-09 18:50:12 diff --git a/pyproject.toml b/pyproject.toml index c080231..2b79ef1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dev = [ "scikit-learn>=1.0", "yaml-changelog>=0.3.0", "twine>=4.0.0", + "towncrier>=24.8.0", ] docs = [ "jupyter-book>=1.0", @@ -70,3 +71,36 @@ addopts = "-v --cov=l0 --cov-report=term-missing" dev = [ "build>=1.3.0", ] + +[tool.towncrier] +package = "l0_python" +directory = "changelog.d" +filename = "CHANGELOG.md" +title_format = "## [{version}] - {project_date}" +issue_format = "" +underlines = ["", "", ""] + +[[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