Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ChangeLog
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 9 additions & 6 deletions build_support/releases/extract_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
- ...
Expand All @@ -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::

Expand All @@ -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:
Expand All @@ -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,
Expand Down
49 changes: 45 additions & 4 deletions build_support/releases/test_extract_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)$")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)"
)


Expand Down
6 changes: 5 additions & 1 deletion doc/developer.contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://keepachangelog.com>`__ 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.

Expand All @@ -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"
Expand Down
Loading