From 751c74e273f9fe6b87777d4499d7cc51e4516f15 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 22 Jul 2026 19:21:52 -0400 Subject: [PATCH] Extend automatic ChangeLog extractor to support pre-releases --- ChangeLog | 2 +- build_support/releases/extract_changelog.py | 15 +++--- .../releases/test_extract_changelog.py | 49 +++++++++++++++++-- doc/developer.contributing.rst | 6 ++- 4 files changed, 60 insertions(+), 12 deletions(-) diff --git a/ChangeLog b/ChangeLog index 19e30ae56..86cdeccfa 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,4 @@ -## [17.0.0] - unreleased +## [17.0.0-alpha.1] - unreleased ### Changed - `lcp_solve` no longer silently absorbs internal exceptions if they occur, but instead these propagate out diff --git a/build_support/releases/extract_changelog.py b/build_support/releases/extract_changelog.py index d8db79a5c..90e74103a 100644 --- a/build_support/releases/extract_changelog.py +++ b/build_support/releases/extract_changelog.py @@ -3,7 +3,7 @@ The ChangeLog at the repository root follows the Keep a Changelog format (https://keepachangelog.com). Each release section looks like: - ## [X.Y.Z] - YYYY-MM-DD + ## [X.Y.Z[-alpha.W|-beta.W|-rc.W]] - YYYY-MM-DD ### Added - ... @@ -20,6 +20,7 @@ Run from the repository root:: python build_support/releases/extract_changelog.py X.Y.Z + python build_support/releases/extract_changelog.py X.Y.Z-rc.W Optional arguments:: @@ -36,14 +37,16 @@ def extract(version: str, changelog: pathlib.Path, output: pathlib.Path) -> None: """Extract the release notes for *version* from *changelog* and write to *output*. - Searches for a section header of the form ``## [X.Y.Z] - ...`` and captures - everything up to the next version header (or end of file). Exits with a - non-zero status and an error message if the version is not found. + Searches for a section header of the form ``## [X.Y.Z] - ...`` or + ``## [X.Y.Z-rc.W] - ...`` and captures everything up to the next version + header (or end of file). Exits with a non-zero status and an error message + if the version is not found. Parameters ---------- version: - Version string without a leading ``v``, e.g. ``"16.6.0"``. + Version string without a leading ``v``, e.g. ``"16.6.0"`` or + ``"17.0.0-rc.1"``. changelog: Path to the ChangeLog file to read from. output: @@ -64,7 +67,7 @@ def extract(version: str, changelog: pathlib.Path, output: pathlib.Path) -> None if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("version", help="Version string, e.g. 16.6.0") + parser.add_argument("version", help="Version string, e.g. 16.6.0 or 17.0.0-rc.1") parser.add_argument( "--changelog", type=pathlib.Path, diff --git a/build_support/releases/test_extract_changelog.py b/build_support/releases/test_extract_changelog.py index bebffa526..7e2db14ff 100644 --- a/build_support/releases/test_extract_changelog.py +++ b/build_support/releases/test_extract_changelog.py @@ -25,9 +25,11 @@ # ChangeLog format rules (Keep a Changelog) # --------------------------------------------------------------------------- -# Pre-release suffixes (a1, b2, rc3) follow PEP 440 conventions used in this project. +# New pre-release identifiers follow Semantic Versioning. Historical PEP 440 +# spellings remain valid so that existing ChangeLog entries continue to pass. VERSION_HEADER_RE = re.compile( - r"^## \[\d+\.\d+\.\d+(?:a\d+|b\d+|rc\d+)?\] - (\d{4}-\d{2}-\d{2}|unreleased)$" + r"^## \[\d+\.\d+\.\d+(?:(?:a|b|rc)\d+|-(?:alpha|beta|rc)\.\d+)?\] - " + r"(\d{4}-\d{2}-\d{2}|unreleased)$" ) # 'General' is a project-specific extension used for cross-cutting changes. SECTION_HEADER_RE = re.compile(r"^### (Added|Changed|Deprecated|Removed|Fixed|Security|General)$") @@ -66,6 +68,25 @@ + SAMPLE_CHANGELOG ) +PRERELEASE_CHANGELOG = """\ +# Changelog + +## [17.0.0-rc.1] - 2026-08-03 + +### Fixed +- Release candidate fix + +## [17.0.0-beta.2] - 2026-07-27 + +### Changed +- Second beta change + +## [17.0.0-alpha.3] - 2026-07-20 + +### Added +- Third alpha feature +""" + # --------------------------------------------------------------------------- # Extraction unit tests @@ -114,6 +135,24 @@ def test_extract_unreleased_version(tmp_path): assert "Upcoming feature" in output.read_text() +@pytest.mark.parametrize( + ("version", "expected"), + [ + ("17.0.0-alpha.3", "Third alpha feature"), + ("17.0.0-beta.2", "Second beta change"), + ("17.0.0-rc.1", "Release candidate fix"), + ], +) +def test_extract_prerelease_version(tmp_path, version, expected): + changelog = tmp_path / "ChangeLog" + changelog.write_text(PRERELEASE_CHANGELOG) + output = tmp_path / "notes.md" + + extract(version, changelog, output) + + assert expected in output.read_text() + + def test_extract_missing_version_exits(tmp_path): changelog = tmp_path / "ChangeLog" changelog.write_text(SAMPLE_CHANGELOG) @@ -151,10 +190,12 @@ def _changelog_lines(): [(line_number, line) for line_number, line in _changelog_lines() if line.startswith("## ")], ) def test_version_header_format(lineno, line): - """Every '## ' line must match '## [X.Y.Z] - YYYY-MM-DD' or '## [X.Y.Z] - unreleased'.""" + """Every '## ' line must contain a supported release version and date.""" assert VERSION_HEADER_RE.match(line), ( f"ChangeLog line {lineno}: invalid version header: {line!r}\n" - "Expected: ## [X.Y.Z] - YYYY-MM-DD or ## [X.Y.Z] - unreleased" + "Expected: ## [X.Y.Z] - DATE or " + "## [X.Y.Z-(alpha|beta|rc).W] - DATE; DATE is YYYY-MM-DD or unreleased " + "(historical PEP 440 prerelease versions are also accepted)" ) diff --git a/doc/developer.contributing.rst b/doc/developer.contributing.rst index ed7e13826..cacccf445 100644 --- a/doc/developer.contributing.rst +++ b/doc/developer.contributing.rst @@ -285,7 +285,8 @@ When making a new release of Gambit, follow these steps: (see :ref:`releases`) and used to populate the GitHub release automatically. The ``ChangeLog`` must follow the `Keep a Changelog `__ format: - version headers of the form ``## [X.Y.Z] - YYYY-MM-DD`` and subsections from + version headers of the form ``## [X.Y.Z] - YYYY-MM-DD`` (or + ``## [X.Y.Z-(alpha|beta|rc).W] - YYYY-MM-DD`` for prereleases) and subsections from ``Added``, ``Changed``, ``Deprecated``, ``Removed``, ``Fixed``, or ``Security``. The test suite enforces this format — any malformed entry will cause ``pytest`` to fail. @@ -294,6 +295,9 @@ When making a new release of Gambit, follow these steps: python build_support/releases/extract_changelog.py X.Y.Z + For a prerelease, pass the complete version (for example, + ``X.Y.Z-rc.W``). + 4. Once there are no further commits to be made for the release, create a tag for the release from the latest commit on the maintenance branch. :: git tag -a vX.Y.Z -m "Gambit version X.Y.Z"