From 00d70106772fc2cc2c1df2980c70ad3c1b9c93de Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 19 Jun 2026 11:49:31 +0100 Subject: [PATCH 01/11] docs: Improve & automate release process --- .github/workflows/release.yml | 49 +++++++++++++++++ doc/changelog.md | 1 + doc/conf.py | 3 +- doc/developer.contributing.rst | 10 ++-- doc/index.rst | 12 +++++ doc/install.rst | 20 ++++--- doc/releases.rst | 99 ++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 8 files changed, 180 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/release.yml create mode 120000 doc/changelog.md create mode 100644 doc/releases.rst diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..4811f025a9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,49 @@ +name: Create GitHub release + +on: + push: + tags: + - 'v*.*.*' + +permissions: + contents: write + +jobs: + create-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Extract changelog for this version + id: changelog + run: | + python3 - <<'EOF' + import re, sys, pathlib + + tag = "${{ github.ref_name }}" # e.g. v16.6.0 + version = tag.lstrip("v") # e.g. 16.6.0 + + text = pathlib.Path("ChangeLog").read_text() + + # Match the section header, e.g. "## [16.6.0] - 2026-03-24" + pattern = re.compile( + rf"^## \[{re.escape(version)}\][^\n]*\n(.*?)(?=^## \[|\Z)", + re.MULTILINE | re.DOTALL, + ) + match = pattern.search(text) + if not match: + print(f"No ChangeLog entry found for version {version}", file=sys.stderr) + sys.exit(1) + + notes = match.group(1).strip() + pathlib.Path("release_notes.md").write_text(notes) + print(f"Extracted release notes for {version}") + EOF + + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${{ github.ref_name }}" \ + --title "Gambit ${{ github.ref_name }}" \ + --notes-file release_notes.md diff --git a/doc/changelog.md b/doc/changelog.md new file mode 120000 index 0000000000..22ec9b8a19 --- /dev/null +++ b/doc/changelog.md @@ -0,0 +1 @@ +../ChangeLog \ No newline at end of file diff --git a/doc/conf.py b/doc/conf.py index 18796ca96f..8638033a5d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -49,6 +49,7 @@ "sphinxcontrib.tikz", "jupyter_sphinx", "sphinxcontrib.bibtex", + "myst_parser", ] @@ -212,7 +213,7 @@ def get_techreport_template(self, e): templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = ".rst" +source_suffix = {".rst": "restructuredtext", ".md": "markdown"} # The encoding of source files. # source_encoding = 'utf-8' diff --git a/doc/developer.contributing.rst b/doc/developer.contributing.rst index 72b35612e8..5007e18756 100644 --- a/doc/developer.contributing.rst +++ b/doc/developer.contributing.rst @@ -280,7 +280,9 @@ When making a new release of Gambit, follow these steps: - `doc/conf.py` reads from GAMBIT_VERSION file at documentation build time - Documentation pages reference the `|release|` substitution variable to automatically reflect the updated version number. -3. Update the `ChangeLog` file with a summary of changes +3. Update the ``ChangeLog`` file at the repository root with a summary of changes for this release. + This file is the single source of truth for release notes — it is surfaced in the documentation + (see :ref:`releases`) and used to populate the GitHub release automatically. 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. :: @@ -291,8 +293,10 @@ When making a new release of Gambit, follow these steps: git push origin maintX_Y git push origin --tags -6. Create a new release on the `GitHub releases page `__, using the tag created in step 4. - Include a summary of changes from the `ChangeLog` file in the release notes. +6. Pushing the tag triggers the ``release.yml`` GitHub Actions workflow, which automatically + reads the ``ChangeLog`` entry for the new version and creates the corresponding release on the + `GitHub releases page `__ with those notes. + No manual action is required. 7. Currently there is no automated process for pushing the new release to PyPI. This must be done manually. diff --git a/doc/index.rst b/doc/index.rst index 4d9cd59b0e..bf3d2cfb32 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -21,6 +21,17 @@ construction and analysis of finite extensive and strategic games. :color: secondary :expand: + .. grid-item-card:: 📦 Releases & Downloads + :columns: 3 + + Download Gambit for Windows, macOS, or Linux. + + .. button-ref:: releases + :ref-type: ref + :click-parent: + :color: secondary + :expand: + .. grid-item-card:: 🐍 PyGambit :columns: 3 @@ -106,6 +117,7 @@ construction and analysis of finite extensive and strategic games. :maxdepth: 1 install + releases algorithms pygambit tools diff --git a/doc/install.rst b/doc/install.rst index 8bb9de061d..7d3a980891 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -18,33 +18,31 @@ PyGambit is available on `PyPI `_. We recomm Older releases can be installed by specifying the version number. -Visit the `Gambit releases page on GitHub `_ for information on older versions. +Visit the :ref:`releases` page for information on older versions. .. _install-cli-gui: Installing Gambit GUI & CLI tools --------------------------------- -To install the Gambit :ref:`GUI ` and :ref:`CLI tools `, visit the `Gambit releases page on GitHub `_ and download the appropriate installer or package for your operating system. -Each release includes pre-built binaries for Windows, macOS, and Linux distributions, accessible under the "Assets" section of each release. +To install the Gambit :ref:`GUI ` and :ref:`CLI tools `, visit the :ref:`releases` page +and download the appropriate installer or package for your operating system. +Each release includes pre-built binaries for Windows, macOS, and Linux distributions. .. dropdown:: Install on macOS with disk image :class-container: sd-border-0 1. **Download the .dmg installer:** - Visit the `Gambit releases page on GitHub `_ and download the `.dmg` file for the version of Gambit you wish to install. + Visit the :ref:`releases` page and download the `.dmg` file for the version of Gambit you wish to install. 2. **Install the application:** Double click the `.dmg` file to mount it, then drag the Gambit application to your Applications folder. .. warning:: - You may need to adjust your macOS security settings to allow the installation of applications from unidentified developers. - - This can be done in ``System Preferences > Security & Privacy`` (see `Apple's documentation `_ for more details). - - If your administration privileges prevent this, try the Homebrew installation method below, or build from source as described in the :ref:`developer build instructions `. + Gambit's macOS application is not signed or notarized by Apple. + See the :ref:`releases` page for guidance on opening it and alternative installation methods. .. dropdown:: Install on macOS via Homebrew :class-container: sd-border-0 @@ -65,7 +63,7 @@ Each release includes pre-built binaries for Windows, macOS, and Linux distribut 1. **Download the source tarball:** - Visit the `Gambit releases page on GitHub `_ and download the source tarball for the version of Gambit you wish to install. + Visit the :ref:`releases` page and download the source tarball for the version of Gambit you wish to install. 2. **Extract the tarball:** @@ -106,7 +104,7 @@ Each release includes pre-built binaries for Windows, macOS, and Linux distribut 1. **Download the installer:** - Visit the `Gambit releases page on GitHub `_ and download the `.msi`. + Visit the :ref:`releases` page and download the `.msi`. 2. **Run the installer:** diff --git a/doc/releases.rst b/doc/releases.rst new file mode 100644 index 0000000000..4fa31bb438 --- /dev/null +++ b/doc/releases.rst @@ -0,0 +1,99 @@ +.. _releases: + +Releases & Downloads +==================== + +The current stable release of Gambit is **|release|**. + +Downloads +--------- + +.. tab-set:: + + .. tab-item:: PyGambit (Python) + + PyGambit is available on `PyPI `_ and is the + recommended way to use Gambit from Python:: + + pip install pygambit + + To install a specific older version:: + + pip install pygambit==X.Y.Z + + .. tab-item:: Windows + + Download the Windows installer (`.msi`) for Gambit |release| from the + `GitHub releases page `__. + + Double-click the downloaded `.msi` file and follow the on-screen instructions. + + .. tab-item:: macOS + + **Disk image (.dmg)** + + Download the macOS disk image for Gambit |release| from the + `GitHub releases page `__. + + Double-click the `.dmg` to mount it, then drag Gambit to your Applications folder. + + .. warning:: + Gambit's macOS application is **not signed or notarized** by Apple. + Gatekeeper will block opening it by default. + + To open it anyway, right-click (or Control-click) the Gambit application and choose + **Open** from the context menu, then confirm in the dialog that appears. + Alternatively, go to **System Settings → Privacy & Security** and click **Open Anyway** + after the first blocked launch attempt. + + If your administrator privileges prevent this, use the Homebrew installation + or build from source (see the :ref:`developer build instructions `). + + Signing and notarization for macOS releases is tracked in + `issue #712 `__. + + **Homebrew** + + .. code-block:: bash + + brew install gambit + + .. warning:: + Homebrew installation has not been set up or tested by the Gambit development team. + + .. tab-item:: Linux (source tarball) + + Download the source tarball for Gambit |release| from the + `GitHub releases page `__. + + Extract and build:: + + tar -xzf gambit-|release|.tar.gz + cd gambit-|release| + ./configure + make + sudo make install + + Run ``./configure --help`` for available options, including ``--prefix`` to change + the installation directory. + + .. warning:: + The graphical interface relies on the other Gambit executables built in this + process. Install the executables to a directory that is in your ``PATH``. + +Older releases +-------------- + +All past releases are available on the +`GitHub releases page `__. +See the :ref:`install` page for platform-specific installation instructions. + +Full changelog +-------------- + +.. toctree:: + :hidden: + + changelog + +:doc:`View full changelog ` diff --git a/pyproject.toml b/pyproject.toml index c318f952a9..ebef656add 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ doc = [ "jupyter_sphinx", "pyyaml", "sphinxcontrib-bibtex", + "myst-parser", ] [project.urls] From f5eaa2315daa3d2d2a095a68f0c12aca4c7737c7 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 19 Jun 2026 12:56:12 +0100 Subject: [PATCH 02/11] refactor: merge release documentation into install guide --- doc/index.rst | 13 ------- doc/install.rst | 74 +++++++++++++++++++++++++----------- doc/releases.rst | 99 ------------------------------------------------ 3 files changed, 52 insertions(+), 134 deletions(-) delete mode 100644 doc/releases.rst diff --git a/doc/index.rst b/doc/index.rst index bf3d2cfb32..c4fa8119dc 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -21,18 +21,6 @@ construction and analysis of finite extensive and strategic games. :color: secondary :expand: - .. grid-item-card:: 📦 Releases & Downloads - :columns: 3 - - Download Gambit for Windows, macOS, or Linux. - - .. button-ref:: releases - :ref-type: ref - :click-parent: - :color: secondary - :expand: - - .. grid-item-card:: 🐍 PyGambit :columns: 3 @@ -117,7 +105,6 @@ construction and analysis of finite extensive and strategic games. :maxdepth: 1 install - releases algorithms pygambit tools diff --git a/doc/install.rst b/doc/install.rst index 7d3a980891..e55ab9b7b9 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -1,8 +1,8 @@ .. _install: -Install -======= +Install & Downloads +=================== Users installing Gambit have several options depending on their needs and their operating system. We recommended most new users install the PyGambit package and read the :ref:`PyGambit documentation `. @@ -16,33 +16,57 @@ PyGambit is available on `PyPI `_. We recomm pip install pygambit +To install a specific older version:: -Older releases can be installed by specifying the version number. -Visit the :ref:`releases` page for information on older versions. + pip install pygambit==X.Y.Z -.. _install-cli-gui: +.. _releases: + +Releases & Downloads +-------------------- + +The current stable release of Gambit is **|release|**. + +.. dropdown:: Install on Windows + :class-container: sd-border-0 + + 1. **Download the installer:** + + Download the `.msi` for Gambit |release| from the + `GitHub releases page `__. + + 2. **Run the installer:** -Installing Gambit GUI & CLI tools ---------------------------------- + Double-click the downloaded `.msi` file and follow the on-screen instructions to complete the installation. -To install the Gambit :ref:`GUI ` and :ref:`CLI tools `, visit the :ref:`releases` page -and download the appropriate installer or package for your operating system. -Each release includes pre-built binaries for Windows, macOS, and Linux distributions. +.. _install-cli-gui: .. dropdown:: Install on macOS with disk image :class-container: sd-border-0 1. **Download the .dmg installer:** - Visit the :ref:`releases` page and download the `.dmg` file for the version of Gambit you wish to install. + Download the `.dmg` for Gambit |release| from the + `GitHub releases page `__. 2. **Install the application:** - Double click the `.dmg` file to mount it, then drag the Gambit application to your Applications folder. + Double-click the `.dmg` file to mount it, then drag the Gambit application to your Applications folder. .. warning:: - Gambit's macOS application is not signed or notarized by Apple. - See the :ref:`releases` page for guidance on opening it and alternative installation methods. + Gambit's macOS application is **not signed or notarized** by Apple. + Gatekeeper will block opening it by default. + + To open it anyway, right-click (or Control-click) the Gambit application and choose + **Open** from the context menu, then confirm in the dialog that appears. + Alternatively, go to **System Settings → Privacy & Security** and click **Open Anyway** + after the first blocked launch attempt. + + If your administrator privileges prevent this, use the Homebrew installation + or build from source (see the :ref:`developer build instructions `). + + Signing and notarization for macOS releases is tracked in + `issue #712 `__. .. dropdown:: Install on macOS via Homebrew :class-container: sd-border-0 @@ -63,7 +87,8 @@ Each release includes pre-built binaries for Windows, macOS, and Linux distribut 1. **Download the source tarball:** - Visit the :ref:`releases` page and download the source tarball for the version of Gambit you wish to install. + Download the source tarball for Gambit |release| from the + `GitHub releases page `__. 2. **Extract the tarball:** @@ -71,7 +96,7 @@ Each release includes pre-built binaries for Windows, macOS, and Linux distribut .. code-block:: bash - tar -xzf gambit-*.tar.gz + tar -xzf gambit-|release|.tar.gz 3. **Build and install Gambit:** @@ -99,13 +124,18 @@ Each release includes pre-built binaries for Windows, macOS, and Linux distribut equilibria. It is strongly recommended that you install the Gambit executables to a directory in your path! -.. dropdown:: Install on Windows with installer - :class-container: sd-border-0 +Older releases +-------------- - 1. **Download the installer:** +All past releases are available on the +`GitHub releases page `__. - Visit the :ref:`releases` page and download the `.msi`. +Full changelog +-------------- - 2. **Run the installer:** +.. toctree:: + :hidden: + + changelog - Double click the downloaded `.msi` file and follow the on-screen instructions to complete the installation. +:doc:`View full changelog ` diff --git a/doc/releases.rst b/doc/releases.rst deleted file mode 100644 index 4fa31bb438..0000000000 --- a/doc/releases.rst +++ /dev/null @@ -1,99 +0,0 @@ -.. _releases: - -Releases & Downloads -==================== - -The current stable release of Gambit is **|release|**. - -Downloads ---------- - -.. tab-set:: - - .. tab-item:: PyGambit (Python) - - PyGambit is available on `PyPI `_ and is the - recommended way to use Gambit from Python:: - - pip install pygambit - - To install a specific older version:: - - pip install pygambit==X.Y.Z - - .. tab-item:: Windows - - Download the Windows installer (`.msi`) for Gambit |release| from the - `GitHub releases page `__. - - Double-click the downloaded `.msi` file and follow the on-screen instructions. - - .. tab-item:: macOS - - **Disk image (.dmg)** - - Download the macOS disk image for Gambit |release| from the - `GitHub releases page `__. - - Double-click the `.dmg` to mount it, then drag Gambit to your Applications folder. - - .. warning:: - Gambit's macOS application is **not signed or notarized** by Apple. - Gatekeeper will block opening it by default. - - To open it anyway, right-click (or Control-click) the Gambit application and choose - **Open** from the context menu, then confirm in the dialog that appears. - Alternatively, go to **System Settings → Privacy & Security** and click **Open Anyway** - after the first blocked launch attempt. - - If your administrator privileges prevent this, use the Homebrew installation - or build from source (see the :ref:`developer build instructions `). - - Signing and notarization for macOS releases is tracked in - `issue #712 `__. - - **Homebrew** - - .. code-block:: bash - - brew install gambit - - .. warning:: - Homebrew installation has not been set up or tested by the Gambit development team. - - .. tab-item:: Linux (source tarball) - - Download the source tarball for Gambit |release| from the - `GitHub releases page `__. - - Extract and build:: - - tar -xzf gambit-|release|.tar.gz - cd gambit-|release| - ./configure - make - sudo make install - - Run ``./configure --help`` for available options, including ``--prefix`` to change - the installation directory. - - .. warning:: - The graphical interface relies on the other Gambit executables built in this - process. Install the executables to a directory that is in your ``PATH``. - -Older releases --------------- - -All past releases are available on the -`GitHub releases page `__. -See the :ref:`install` page for platform-specific installation instructions. - -Full changelog --------------- - -.. toctree:: - :hidden: - - changelog - -:doc:`View full changelog ` From 184c5607382f9a761ed57251b633bdadf5ce55e6 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 19 Jun 2026 13:16:17 +0100 Subject: [PATCH 03/11] Just install --- doc/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install.rst b/doc/install.rst index e55ab9b7b9..64e862468a 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -1,7 +1,7 @@ .. _install: -Install & Downloads +Install =================== Users installing Gambit have several options depending on their needs and their operating system. From f9c2abcf09ac8af5066bf699ab7dbc7737f7e271 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 19 Jun 2026 13:19:07 +0100 Subject: [PATCH 04/11] Remove issue link --- doc/install.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/install.rst b/doc/install.rst index 64e862468a..7cc6f0a8db 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -65,9 +65,6 @@ The current stable release of Gambit is **|release|**. If your administrator privileges prevent this, use the Homebrew installation or build from source (see the :ref:`developer build instructions `). - Signing and notarization for macOS releases is tracked in - `issue #712 `__. - .. dropdown:: Install on macOS via Homebrew :class-container: sd-border-0 From e18e88a4f6d29c3482252827ee32db72218d8a1d Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 19 Jun 2026 13:33:56 +0100 Subject: [PATCH 05/11] docs: simplify release link management with rst_epilog --- doc/conf.py | 5 +++++ doc/install.rst | 11 ++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 8638033a5d..45175ea3fa 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -241,6 +241,11 @@ def get_techreport_template(self, e): # Throw error if GAMBIT_VERSION file not found raise FileNotFoundError("GAMBIT_VERSION file not found") +_release_url = f"https://github.com/gambitproject/gambit/releases/tag/v{_full_version}" +rst_epilog = f""" +.. |release_link| replace:: `GitHub releases page <{_release_url}>`__ +""" + # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None diff --git a/doc/install.rst b/doc/install.rst index 7cc6f0a8db..b49db9220c 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -25,15 +25,14 @@ To install a specific older version:: Releases & Downloads -------------------- -The current stable release of Gambit is **|release|**. +The current stable release of Gambit is |release|. .. dropdown:: Install on Windows :class-container: sd-border-0 1. **Download the installer:** - Download the `.msi` for Gambit |release| from the - `GitHub releases page `__. + Download the `.msi` for Gambit |release| from the |release_link|. 2. **Run the installer:** @@ -46,8 +45,7 @@ The current stable release of Gambit is **|release|**. 1. **Download the .dmg installer:** - Download the `.dmg` for Gambit |release| from the - `GitHub releases page `__. + Download the `.dmg` for Gambit |release| from the |release_link|. 2. **Install the application:** @@ -84,8 +82,7 @@ The current stable release of Gambit is **|release|**. 1. **Download the source tarball:** - Download the source tarball for Gambit |release| from the - `GitHub releases page `__. + Download the source tarball for Gambit |release| from the |release_link|. 2. **Extract the tarball:** From e752d22976cbdfd48768600dac9102394f9a85d4 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 19 Jun 2026 13:58:15 +0100 Subject: [PATCH 06/11] feat: implement automated ChangeLog extraction for releases with supporting tests and documentation --- .github/workflows/release.yml | 25 +-- build_support/releases/extract_changelog.py | 81 +++++++++ .../releases/test_extract_changelog.py | 171 ++++++++++++++++++ doc/developer.contributing.rst | 10 + pyproject.toml | 2 +- 5 files changed, 265 insertions(+), 24 deletions(-) create mode 100644 build_support/releases/extract_changelog.py create mode 100644 build_support/releases/test_extract_changelog.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4811f025a9..b4d4ef171a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,30 +15,9 @@ jobs: - uses: actions/checkout@v6 - name: Extract changelog for this version - id: changelog run: | - python3 - <<'EOF' - import re, sys, pathlib - - tag = "${{ github.ref_name }}" # e.g. v16.6.0 - version = tag.lstrip("v") # e.g. 16.6.0 - - text = pathlib.Path("ChangeLog").read_text() - - # Match the section header, e.g. "## [16.6.0] - 2026-03-24" - pattern = re.compile( - rf"^## \[{re.escape(version)}\][^\n]*\n(.*?)(?=^## \[|\Z)", - re.MULTILINE | re.DOTALL, - ) - match = pattern.search(text) - if not match: - print(f"No ChangeLog entry found for version {version}", file=sys.stderr) - sys.exit(1) - - notes = match.group(1).strip() - pathlib.Path("release_notes.md").write_text(notes) - print(f"Extracted release notes for {version}") - EOF + VERSION="${{ github.ref_name }}" + python3 build_support/releases/extract_changelog.py "${VERSION#v}" - name: Create GitHub release env: diff --git a/build_support/releases/extract_changelog.py b/build_support/releases/extract_changelog.py new file mode 100644 index 0000000000..7b87473676 --- /dev/null +++ b/build_support/releases/extract_changelog.py @@ -0,0 +1,81 @@ +"""Extract the changelog section for a given version from the ChangeLog file. + +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 + + ### Added + - ... + + ### Fixed + - ... + +This script locates the section for a given version and writes it to an output +file, which is then used by the ``release.yml`` GitHub Actions workflow to +populate the GitHub release notes. + +Usage +----- +Run from the repository root:: + + python3 build_support/releases/extract_changelog.py X.Y.Z + +Optional arguments:: + + --changelog PATH Path to the ChangeLog file (default: ChangeLog) + --output PATH Path to write extracted notes (default: release_notes.md) +""" + +import argparse +import pathlib +import re +import sys + + +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. + + Parameters + ---------- + version: + Version string without a leading ``v``, e.g. ``"16.6.0"``. + changelog: + Path to the ChangeLog file to read from. + output: + Path to the file to write the extracted notes to. + """ + text = changelog.read_text() + pattern = re.compile( + rf"^## \[{re.escape(version)}\][^\n]*\n(.*?)(?=^## \[|\Z)", + re.MULTILINE | re.DOTALL, + ) + match = pattern.search(text) + if not match: + print(f"No ChangeLog entry found for version {version}", file=sys.stderr) + sys.exit(1) + output.write_text(match.group(1).strip()) + print(f"Extracted release notes for {version}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("version", help="Version string, e.g. 16.6.0") + parser.add_argument( + "--changelog", + type=pathlib.Path, + default=pathlib.Path("ChangeLog"), + help="Path to the ChangeLog file (default: ChangeLog)", + ) + parser.add_argument( + "--output", + type=pathlib.Path, + default=pathlib.Path("release_notes.md"), + help="Path to write the extracted notes (default: release_notes.md)", + ) + args = parser.parse_args() + extract(args.version, args.changelog, args.output) diff --git a/build_support/releases/test_extract_changelog.py b/build_support/releases/test_extract_changelog.py new file mode 100644 index 0000000000..826f983b7d --- /dev/null +++ b/build_support/releases/test_extract_changelog.py @@ -0,0 +1,171 @@ +"""Tests for extract_changelog.py. + +Covers two concerns: +1. Correctness of the extraction logic (unit tests against fixture content). +2. Format validity of the repository's actual ChangeLog file. + +The format tests act as a CI guard: if a contributor adds a malformed version +header or an unrecognised section type to ChangeLog, the test suite will fail. +""" + +import pathlib +import re + +import pytest +from extract_changelog import extract + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- + +REPO_ROOT = pathlib.Path(__file__).parent.parent.parent +CHANGELOG = REPO_ROOT / "ChangeLog" + +# --------------------------------------------------------------------------- +# ChangeLog format rules (Keep a Changelog) +# --------------------------------------------------------------------------- + +# Pre-release suffixes (a1, b2, rc3) follow PEP 440 conventions used in this project. +VERSION_HEADER_RE = re.compile( + r"^## \[\d+\.\d+\.\d+(?:a\d+|b\d+|rc\d+)?\] - (\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)$") + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +SAMPLE_CHANGELOG = """\ +# Changelog + +## [2.0.0] - 2024-01-15 + +### Added +- Feature A + +### Fixed +- Bug B + +## [1.0.0] - 2023-06-01 + +### Added +- Initial release +""" + +UNRELEASED_CHANGELOG = ( + """\ +# Changelog + +## [3.0.0] - unreleased + +### Added +- Upcoming feature + +""" + + SAMPLE_CHANGELOG +) + + +# --------------------------------------------------------------------------- +# Extraction unit tests +# --------------------------------------------------------------------------- + + +def test_extract_known_version(tmp_path): + changelog = tmp_path / "ChangeLog" + changelog.write_text(SAMPLE_CHANGELOG) + output = tmp_path / "notes.md" + + extract("2.0.0", changelog, output) + + content = output.read_text() + assert "Feature A" in content + assert "Bug B" in content + + +def test_extract_does_not_bleed_into_next_version(tmp_path): + changelog = tmp_path / "ChangeLog" + changelog.write_text(SAMPLE_CHANGELOG) + output = tmp_path / "notes.md" + + extract("2.0.0", changelog, output) + + assert "Initial release" not in output.read_text() + + +def test_extract_last_version_in_file(tmp_path): + changelog = tmp_path / "ChangeLog" + changelog.write_text(SAMPLE_CHANGELOG) + output = tmp_path / "notes.md" + + extract("1.0.0", changelog, output) + + assert "Initial release" in output.read_text() + + +def test_extract_unreleased_version(tmp_path): + changelog = tmp_path / "ChangeLog" + changelog.write_text(UNRELEASED_CHANGELOG) + output = tmp_path / "notes.md" + + extract("3.0.0", changelog, output) + + assert "Upcoming feature" in output.read_text() + + +def test_extract_missing_version_exits(tmp_path): + changelog = tmp_path / "ChangeLog" + changelog.write_text(SAMPLE_CHANGELOG) + output = tmp_path / "notes.md" + + with pytest.raises(SystemExit) as exc_info: + extract("99.0.0", changelog, output) + + assert exc_info.value.code != 0 + + +def test_extract_output_is_stripped(tmp_path): + changelog = tmp_path / "ChangeLog" + changelog.write_text(SAMPLE_CHANGELOG) + output = tmp_path / "notes.md" + + extract("2.0.0", changelog, output) + + content = output.read_text() + assert content == content.strip() + + +# --------------------------------------------------------------------------- +# ChangeLog format validation tests +# --------------------------------------------------------------------------- + + +def _changelog_lines(): + """Return (line_number, line) pairs for the repository ChangeLog.""" + return list(enumerate(CHANGELOG.read_text().splitlines(), start=1)) + + +@pytest.mark.parametrize( + "lineno,line", + [(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'.""" + 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" + ) + + +@pytest.mark.parametrize( + "lineno,line", + [(line_number, line) for line_number, line in _changelog_lines() if line.startswith("## ")], +) +def test_section_header_type(lineno, line): + """Every '### ' line must be one of the recognised Keep a Changelog types.""" + assert SECTION_HEADER_RE.match(line), ( + f"ChangeLog line {lineno}: unrecognised section header: {line!r}\n" + "Allowed: ### Added, ### Changed, ### Deprecated, " + "### Removed, ### Fixed, ### Security" + ) diff --git a/doc/developer.contributing.rst b/doc/developer.contributing.rst index 5007e18756..46266c9c79 100644 --- a/doc/developer.contributing.rst +++ b/doc/developer.contributing.rst @@ -284,6 +284,16 @@ When making a new release of Gambit, follow these steps: This file is the single source of truth for release notes — it is surfaced in the documentation (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 + ``Added``, ``Changed``, ``Deprecated``, ``Removed``, ``Fixed``, or ``Security``. + The test suite enforces this format — any malformed entry will cause ``pytest`` to fail. + + To verify the new entry will be extracted correctly before tagging, run the + extraction script from the repository root:: + + python3 build_support/releases/extract_changelog.py X.Y.Z + 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" diff --git a/pyproject.toml b/pyproject.toml index ebef656add..39bb2d86a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ max-line-length = 99 [tool.pytest.ini_options] addopts = "--strict-markers" -pythonpath = ["build_support/catalog"] +pythonpath = ["build_support/catalog", "build_support/releases"] markers = [ "nash_enumpure_strategy: tests of enumpure_solve in pure strategies", "nash_enumpure_agent: tests of enumpure_solve in pure behaviors", From 9c9eb8b7ab7a5723450c3a7f51c170e6405c5dab Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 19 Jun 2026 14:12:34 +0100 Subject: [PATCH 07/11] Fix test changelog test header level problem --- build_support/releases/test_extract_changelog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_support/releases/test_extract_changelog.py b/build_support/releases/test_extract_changelog.py index 826f983b7d..bebffa5260 100644 --- a/build_support/releases/test_extract_changelog.py +++ b/build_support/releases/test_extract_changelog.py @@ -160,7 +160,7 @@ def test_version_header_format(lineno, line): @pytest.mark.parametrize( "lineno,line", - [(line_number, line) for line_number, line in _changelog_lines() if line.startswith("## ")], + [(line_number, line) for line_number, line in _changelog_lines() if line.startswith("### ")], ) def test_section_header_type(lineno, line): """Every '### ' line must be one of the recognised Keep a Changelog types.""" From 7af2b33422043d4c1b4a955d80f5404c1e968544 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 19 Jun 2026 14:24:59 +0100 Subject: [PATCH 08/11] docs: replace changelog link with direct inclusion --- doc/changelog.md | 1 - doc/install.rst | 8 ++------ 2 files changed, 2 insertions(+), 7 deletions(-) delete mode 120000 doc/changelog.md diff --git a/doc/changelog.md b/doc/changelog.md deleted file mode 120000 index 22ec9b8a19..0000000000 --- a/doc/changelog.md +++ /dev/null @@ -1 +0,0 @@ -../ChangeLog \ No newline at end of file diff --git a/doc/install.rst b/doc/install.rst index b49db9220c..5d0dddb3f9 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -127,9 +127,5 @@ All past releases are available on the Full changelog -------------- -.. toctree:: - :hidden: - - changelog - -:doc:`View full changelog ` +.. include:: ../ChangeLog + :parser: myst_parser.sphinx_ From 618976fbf91be154cf2bfceda48743e637119dc3 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 19 Jun 2026 14:28:35 +0100 Subject: [PATCH 09/11] simplify python3 to python in 2 places --- build_support/releases/extract_changelog.py | 2 +- doc/developer.contributing.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build_support/releases/extract_changelog.py b/build_support/releases/extract_changelog.py index 7b87473676..d8db79a5c6 100644 --- a/build_support/releases/extract_changelog.py +++ b/build_support/releases/extract_changelog.py @@ -19,7 +19,7 @@ ----- Run from the repository root:: - python3 build_support/releases/extract_changelog.py X.Y.Z + python build_support/releases/extract_changelog.py X.Y.Z Optional arguments:: diff --git a/doc/developer.contributing.rst b/doc/developer.contributing.rst index 46266c9c79..ed7e138264 100644 --- a/doc/developer.contributing.rst +++ b/doc/developer.contributing.rst @@ -292,7 +292,7 @@ When making a new release of Gambit, follow these steps: To verify the new entry will be extracted correctly before tagging, run the extraction script from the repository root:: - python3 build_support/releases/extract_changelog.py X.Y.Z + python build_support/releases/extract_changelog.py X.Y.Z 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. :: From 7d9670266bfafb59bf99036f58f2cf45f84a4301 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 19 Jun 2026 14:35:21 +0100 Subject: [PATCH 10/11] simplify headers --- doc/install.rst | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/doc/install.rst b/doc/install.rst index 5d0dddb3f9..777bc95787 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -22,8 +22,8 @@ To install a specific older version:: .. _releases: -Releases & Downloads --------------------- +Releases +-------- The current stable release of Gambit is |release|. @@ -118,14 +118,9 @@ The current stable release of Gambit is |release|. equilibria. It is strongly recommended that you install the Gambit executables to a directory in your path! -Older releases --------------- - All past releases are available on the `GitHub releases page `__. -Full changelog --------------- .. include:: ../ChangeLog :parser: myst_parser.sphinx_ From a62bc228eef27f9358805eb83d22af9b31e09744 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 19 Jun 2026 14:37:09 +0100 Subject: [PATCH 11/11] Add Changelog header to docs --- ChangeLog | 2 -- doc/install.rst | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 08a0ee71ae..58888e2a14 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,3 @@ -# Changelog - ## [16.7.0] - unreleased ### Added diff --git a/doc/install.rst b/doc/install.rst index 777bc95787..14be76591b 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -122,5 +122,8 @@ All past releases are available on the `GitHub releases page `__. +Changelog +--------- + .. include:: ../ChangeLog :parser: myst_parser.sphinx_