From d121948704dd01064a148b71612611fd19d273ff Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:24:18 +0100 Subject: [PATCH 01/18] feat: allow 'nanobind' as python binding library, additionally to 'pybind11' and 'None' --- README.md | 4 ++-- cookiecutter.json | 4 ++-- hooks/post_gen_project.py | 8 ++++---- hooks/pre_gen_project.py | 2 +- tests/test_bake_project.py | 8 ++++---- tests/test_deploy_bake.py | 2 +- .../.github/workflows/ci.yml | 10 +++++----- {{cookiecutter.project_slug}}/.gitignore | 2 +- {{cookiecutter.project_slug}}/.gitlab-ci.yml | 2 +- .../.pre-commit-config.yaml | 4 ++-- {{cookiecutter.project_slug}}/CMakeLists.txt | 17 ++++++++++++++--- {{cookiecutter.project_slug}}/FILESTRUCTURE.md | 4 ++-- {{cookiecutter.project_slug}}/README.md | 8 ++++---- {{cookiecutter.project_slug}}/TODO.md | 2 +- {{cookiecutter.project_slug}}/codecov.yml | 2 +- {{cookiecutter.project_slug}}/doc/conf.py | 2 +- {{cookiecutter.project_slug}}/pyproject.toml | 6 +++++- .../_{{cookiecutter.project_slug}}.cpp | 14 +++++++++++++- 18 files changed, 64 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 382046ef..2ed5a30b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The core features of our C++ Cookiecutter in a nutshell: * Ready-to-use integration with the Github Actions and Gitlab CI * Generation and deployment of Sphinx-based documentation for [Read the Docs](https://readthedocs.org) * Building a Doxygen documentation -* Setup of Python bindings with Pybind11 +* Setup of Python bindings with [pybind11](https://github.com/pybind/pybind11) or [nanobind](https://github.com/wjakob/nanobind) * CI-based deployment of Python wheels to PyPI * Generated [pre-commit](https://pre-commit.com/) configuration is added if you have `pre-commit` installed * Integration with coverage testing from [codecov.io](https://codecov.io) @@ -76,7 +76,7 @@ This cookiecutter accepts the following configuration options: * `cxx_minimum_standard`: The minimum C++ standard required for this project. It can be chosen from `14` (default), `17`, `20` and `23`. `C++11` and earlier are not supported, because the generated project will depend on libraries that require `C++14` ([Catch2](https://github.com/catchorg/Catch2) for testing and [pybind11](https://github.com/pybind/pybind11) for potential Python bindings). -* `python_bindings`: Whether to automatically add a PyBind11-based Python binding package. +* `python_bindings`: Whether to automatically add a PyBind11-based Python binding package. It can be chosen from [pybind11](https://github.com/pybind/pybind11) and [nanobind](https://github.com/wjakob/nanobind) or it can be omitted ([None]). * `pypi_release`: Whether to add an automatic PyPI deploy workflow to the CI system. This is currently limited to Github Actions CI as it provides cloud-based runners for all relevant platforms (Linux, MacOS, Windows). A PyPI release is automatically triggered when a release is diff --git a/cookiecutter.json b/cookiecutter.json index 47eddbf9..ec6f5874 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -12,8 +12,8 @@ "readthedocs": ["Yes", "No"], "doxygen": ["Yes", "No"], "cxx_minimum_standard": ["14", "17", "20", "23"], - "python_bindings": ["No", "Yes"], - "pypi_release": "{{ cookiecutter.python_bindings }}", + "python_bindings": ["None", "pybind11", "nanobind"], + "pypi_release": "{%- if cookiecutter.python_bindings == 'None' -%}No{%- else -%}Yes{%- endif -%}", "codecovio": "{{ cookiecutter.github_actions_ci }}", "_extensions": [ "local_extensions.PrecommitExtension", diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 04731480..812f4efe 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -72,10 +72,10 @@ def conditional_remove(condition, path): conditional_remove("{{ cookiecutter.readthedocs }}" == "No", "doc/index.rst") conditional_remove("{{ cookiecutter.readthedocs }}" == "No", "doc/requirements-rtd.txt") conditional_remove("{{ cookiecutter.doxygen }}" == "No" and "{{ cookiecutter.readthedocs }}" == "No", "doc") -conditional_remove("{{ cookiecutter.python_bindings }}" == "No", "pyproject.toml") -conditional_remove("{{ cookiecutter.python_bindings }}" == "No", "requirements-dev.txt") -conditional_remove("{{ cookiecutter.python_bindings }}" == "No", "python") -conditional_remove("{{ cookiecutter.python_bindings }}" == "No", "tests/python") +conditional_remove("{{ cookiecutter.python_bindings }}" == "None", "pyproject.toml") +conditional_remove("{{ cookiecutter.python_bindings }}" == "None", "requirements-dev.txt") +conditional_remove("{{ cookiecutter.python_bindings }}" == "None", "python") +conditional_remove("{{ cookiecutter.python_bindings }}" == "None", "tests/python") conditional_remove("{{ cookiecutter.pypi_release }}" != "Yes", ".github/workflows/pypi.yml") conditional_remove("{{ cookiecutter.codecovio }}" == "No", "codecov.yml") conditional_remove("{{ cookiecutter.github_actions_ci }}" == "No", ".github") diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py index 3998bcb6..2c61972d 100644 --- a/hooks/pre_gen_project.py +++ b/hooks/pre_gen_project.py @@ -25,7 +25,7 @@ def fail_if(condition, message): fail_if( - "{{ cookiecutter.pypi_release }}" != "No" and "{{ cookiecutter.python_bindings }}" == "No", + "{{ cookiecutter.pypi_release }}" != "No" and "{{ cookiecutter.python_bindings }}" == "None", "Can't do PyPI release without building Python bindings" ) diff --git a/tests/test_bake_project.py b/tests/test_bake_project.py index ad060b97..575771c5 100644 --- a/tests/test_bake_project.py +++ b/tests/test_bake_project.py @@ -148,7 +148,7 @@ def test_github_actions_ci(cookies): extra_context={ 'remote_url': 'git@github.com:dokempf/test-gha-cookiecutter.git', 'github_actions_ci': 'Yes', - 'python_bindings': 'Yes', + 'python_bindings': 'pybind11', 'pypi_release': 'Yes', } ) @@ -176,7 +176,7 @@ def test_python(cookies, virtualenv, submodules): bake = cookies.bake( extra_context={ 'project_slug': 'my-project', - 'python_bindings': 'Yes', + 'python_bindings': 'pybind11', 'submodules': submodules, } ) @@ -191,13 +191,13 @@ def test_python(cookies, virtualenv, submodules): @pytest.mark.local def test_pypi_without_python(cookies): - bake = cookies.bake(extra_context={'python_bindings': 'No', 'pypi_release': 'Yes'}) + bake = cookies.bake(extra_context={'python_bindings': 'None', 'pypi_release': 'Yes'}) assert bake.exit_code != 0 @pytest.mark.local def test_pypi_without_github(cookies): - bake = cookies.bake(extra_context={'github_actions_ci': 'No', 'pypi_release': 'Yes', 'python_bindings': 'Yes'}) + bake = cookies.bake(extra_context={'github_actions_ci': 'No', 'pypi_release': 'Yes', 'python_bindings': 'pybind11'}) assert bake.exit_code != 0 diff --git a/tests/test_deploy_bake.py b/tests/test_deploy_bake.py index 36feb962..1ee43336 100644 --- a/tests/test_deploy_bake.py +++ b/tests/test_deploy_bake.py @@ -19,7 +19,7 @@ def test_push_remote(cookies): 'github_actions_ci': 'Yes', 'gitlab_ci': 'Yes', 'readthedocs': 'Yes', - 'python_bindings': 'Yes', + 'python_bindings': 'pybind11', 'pypi_release': 'Yes', 'use_submodules': 'No', 'codecovio': 'Yes', diff --git a/{{cookiecutter.project_slug}}/.github/workflows/ci.yml b/{{cookiecutter.project_slug}}/.github/workflows/ci.yml index f700cdcf..aa86e446 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: shell: bash working-directory: ${{ "{{ github.workspace }}" }}/build run: ctest -{%- if cookiecutter.python_bindings == "Yes" %} +{%- if cookiecutter.python_bindings != "None" %} python-testing: name: Python package testing on ${{ "{{matrix.os}}" }} runs-on: ${{ "{{matrix.os}}" }} @@ -97,10 +97,10 @@ jobs: - name: Install Doxygen uses: ssciwr/doxygen-install@v2 {% endif %} -{% if cookiecutter.python_bindings == "Yes" %} +{% if cookiecutter.python_bindings != "None" %} - name: Install Python package editable run: | - python -m pip install scikit_build_core pybind11[global] + python -m pip install scikit_build_core {% if cookiecutter.python_bindings == "pybind11" -%}pybind11[global]{%- elif cookiecutter.python_bindings == "nanobind" %}nanobind[global]{% endif %} python -m pip install --no-build-isolation --config-settings=build-dir="${{ "{{ github.workspace }}" }}/build" --config-settings=cmake.define.CMAKE_CXX_FLAGS="--coverage" --config-settings=cmake.define.BUILD_TESTING="ON" -v -e . python -m pip install -r requirements-dev.txt @@ -138,5 +138,5 @@ jobs: uses: codecov/codecov-action@v5 with: fail_ci_if_error: true - files: ${{ "{{github.workspace}}" }}/coverage.info{% if cookiecutter.python_bindings == "Yes" %},coverage.xml{% endif %} -{% endif %} \ No newline at end of file + files: ${{ "{{github.workspace}}" }}/coverage.info{% if cookiecutter.python_bindings != "None" %},coverage.xml{% endif %} +{% endif %} diff --git a/{{cookiecutter.project_slug}}/.gitignore b/{{cookiecutter.project_slug}}/.gitignore index 9a190df4..210ad986 100644 --- a/{{cookiecutter.project_slug}}/.gitignore +++ b/{{cookiecutter.project_slug}}/.gitignore @@ -33,7 +33,7 @@ _skbuild *.exe *.out *.app -{% if cookiecutter.python_bindings == "Yes" %} +{% if cookiecutter.python_bindings != 'None' %} # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/{{cookiecutter.project_slug}}/.gitlab-ci.yml b/{{cookiecutter.project_slug}}/.gitlab-ci.yml index f0dcc752..5b3264e3 100644 --- a/{{cookiecutter.project_slug}}/.gitlab-ci.yml +++ b/{{cookiecutter.project_slug}}/.gitlab-ci.yml @@ -9,7 +9,7 @@ variables: - apt update - apt install -y cmake git - echo "Installing potential dependencies..." -{% if cookiecutter.python_bindings == "Yes" %} +{% if cookiecutter.python_bindings != "None" %} - apt install -y python3-dev {%- endif %} {% if cookiecutter.doxygen == "Yes" %} diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml index 8820967c..622bad48 100644 --- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: -{%- if cookiecutter.python_bindings == "Yes" %} +{%- if cookiecutter.python_bindings != "None" %} # Run Black - the uncompromising Python code formatter - repo: https://github.com/psf/black rev: 23.7.0 @@ -25,7 +25,7 @@ repos: - id: trailing-whitespace # Apply a file size limit of 500kB - id: check-added-large-files -{%- if cookiecutter.python_bindings == "Yes" %} +{%- if cookiecutter.python_bindings != "None" %} # Simple parser validation of e.g. pyproject.toml - id: check-toml {%- endif %} diff --git a/{{cookiecutter.project_slug}}/CMakeLists.txt b/{{cookiecutter.project_slug}}/CMakeLists.txt index b354d108..7c3ae5a5 100644 --- a/{{cookiecutter.project_slug}}/CMakeLists.txt +++ b/{{cookiecutter.project_slug}}/CMakeLists.txt @@ -11,7 +11,7 @@ include(GNUInstallDirs) # Define build options option({{ cookiecutter.project_slug }}_BUILD_TESTING "Enable building of tests" OFF) -{%- if cookiecutter.python_bindings == "Yes" %} +{%- if cookiecutter.python_bindings != "None" %} if(PROJECT_IS_TOP_LEVEL) option({{ cookiecutter.project_slug }}_BUILD_PYTHON "Enable building of Python bindings" ON) @@ -79,7 +79,7 @@ if(PROJECT_IS_TOP_LEVEL AND {{ cookiecutter.project_slug }}_BUILD_DOCS) add_subdirectory(doc) endif() {%- endif %} -{% if cookiecutter.python_bindings == "Yes" %} +{% if cookiecutter.python_bindings != "None" %} # Add Python bindings if(PROJECT_IS_TOP_LEVEL AND {{ cookiecutter.project_slug }}_BUILD_PYTHON) # Enable PIC for Python bindings @@ -88,15 +88,26 @@ if(PROJECT_IS_TOP_LEVEL AND {{ cookiecutter.project_slug }}_BUILD_PYTHON) POSITION_INDEPENDENT_CODE ON ) + {% if cookiecutter.python_bindings == "pybind11" -%} set(PYBIND11_FINDPYTHON ON) find_package(pybind11 REQUIRED) # Compile the Pybind11 module pybind11_add_module(_{{ cookiecutter|modname }} python/{{ cookiecutter|modname }}/_{{ cookiecutter.project_slug }}.cpp) + {% elif cookiecutter.python_bindings == "nanobind" %} + find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) + + # Detect the installed nanobind package and import it into CMake + execute_process( + COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT + ) + find_package(nanobind CONFIG REQUIRED) + nanobind_add_module(_{{ cookiecutter|modname }} python/{{ cookiecutter|modname }}/_{{ cookiecutter.project_slug }}.cpp) + {%- endif %} target_link_libraries(_{{ cookiecutter|modname }} PUBLIC {{ cookiecutter.project_slug }} ) - # Install the Python module shared library install(TARGETS _{{ cookiecutter|modname }} DESTINATION .) endif() diff --git a/{{cookiecutter.project_slug}}/FILESTRUCTURE.md b/{{cookiecutter.project_slug}}/FILESTRUCTURE.md index deb7f09d..b0f43b60 100644 --- a/{{cookiecutter.project_slug}}/FILESTRUCTURE.md +++ b/{{cookiecutter.project_slug}}/FILESTRUCTURE.md @@ -11,10 +11,10 @@ This is an explanation of the repository's file structure: * `tests/{{ cookiecutter.project_slug }}_t.cpp` contains the unit tests for the library. The unit tests are written using Catch2. For further reading on what can be achieved with Catch2, we recommend [their tutorial](https://github.com/catchorg/Catch2/blob/devel/docs/tutorial.md). -{%- if cookiecutter.python_bindings == "Yes" %} +{%- if cookiecutter.python_bindings != "None" %} * The `python/{{ cookiecutter.project_slug.replace("-", "") }}` directory contains a Python package for the project. It contains a compiled Python module `_{{ cookiecutter.project_slug.replace("-", "") }}` - that CMake generates from the Pybind11 source file `python/{{ cookiecutter.project_slug.replace("-", "") }}/_{{ cookiecutter.project_slug }}.cpp`. Additionally, it contains pure Python sources (e.g. `__init__.py`) that + that CMake generates from the {% if cookiecutter.python_bindings == "pybind11" -%}Pybind11{% elif cookiecutter.python_bindings == "nanobind" %}nanobind{% endif %} source file `python/{{ cookiecutter.project_slug.replace("-", "") }}/_{{ cookiecutter.project_slug }}.cpp`. Additionally, it contains pure Python sources (e.g. `__init__.py`) that allow to wrap Python functionality around the compiled module. {%- endif %} * CMake build system files diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md index f43d510b..5132b305 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/{{cookiecutter.project_slug}}/README.md @@ -50,7 +50,7 @@ Building {{ cookiecutter.project_name }} requires the following software install {%- if cookiecutter.use_submodules == "No" %} * The testing framework [Catch2](https://github.com/catchorg/Catch2) for building the test suite {%- endif %} -{%- if cookiecutter.python_bindings == "Yes" %} +{%- if cookiecutter.python_bindings != "None" %} * Python `>= 3.10` for building Python bindings {%- endif %} @@ -72,11 +72,11 @@ which can be set by adding `-D={ON, OFF}` to the `cmake` call: {%- if cookiecutter.doxygen == "Yes" or cookiecutter.readthedocs == "Yes" %} * `{{ cookiecutter.project_slug }}_BUILD_DOCS`: Enable building the documentation (default: `ON`) {%- endif %} -{%- if cookiecutter.python_bindings == "Yes" %} +{%- if cookiecutter.python_bindings != "None" %} * `{{ cookiecutter.project_slug }}_BUILD_PYTHON`: Enable building the Python bindings (default: `ON`) {%- endif %} -{% if cookiecutter.python_bindings == "Yes" %} +{% if cookiecutter.python_bindings != "None" %} If you wish to build and install the project as a Python project without having access to C++ build artifacts like libraries and executables, you can do so using `pip` from the root directory: @@ -96,7 +96,7 @@ the C++ test suite of `{{ cookiecutter.project_name }}` can be run using cd build ctest ``` -{% if cookiecutter.python_bindings == "Yes" %} +{% if cookiecutter.python_bindings != "None" %} The Python test suite can be run by first `pip`-installing the Python package and then running `pytest` from the top-level directory: diff --git a/{{cookiecutter.project_slug}}/TODO.md b/{{cookiecutter.project_slug}}/TODO.md index fd35df64..29310458 100644 --- a/{{cookiecutter.project_slug}}/TODO.md +++ b/{{cookiecutter.project_slug}}/TODO.md @@ -39,7 +39,7 @@ The following tasks need to be done to get a fully working project: * Make sure that doxygen is installed on your system, e.g. by doing `sudo apt install doxygen` on Debian or Ubuntu. {%- endif %} -{%- if cookiecutter.python_bindings == "Yes" %} +{%- if cookiecutter.python_bindings != "None" %} * Edit the parameters of `pyproject.toml` file to contain the necessary information about your project, such as your email adress, PyPI classifiers and a short project description. {%- endif %} diff --git a/{{cookiecutter.project_slug}}/codecov.yml b/{{cookiecutter.project_slug}}/codecov.yml index 97598c4e..c36b11be 100644 --- a/{{cookiecutter.project_slug}}/codecov.yml +++ b/{{cookiecutter.project_slug}}/codecov.yml @@ -21,7 +21,7 @@ comment: ignore: - app -{%- if cookiecutter.python_bindings == "Yes" %} +{%- if cookiecutter.python_bindings != "None" %} - python {%- endif %} - "**/tests" diff --git a/{{cookiecutter.project_slug}}/doc/conf.py b/{{cookiecutter.project_slug}}/doc/conf.py index 5096e5dd..f27f02d1 100644 --- a/{{cookiecutter.project_slug}}/doc/conf.py +++ b/{{cookiecutter.project_slug}}/doc/conf.py @@ -66,6 +66,6 @@ cwd = os.getcwd() os.makedirs("build-cmake", exist_ok=True) builddir = os.path.join(cwd, "build-cmake") - subprocess.check_call("cmake -D{{ cookiecutter.project_slug }}_BUILD_DOCS=ON -D{{ cookiecutter.project_slug }}_BUILD_TESTING=OFF {% if cookiecutter.python_bindings == 'Yes' %}-D{{ cookiecutter.project_slug }}_BUILD_PYTHON=OFF{% endif %} ../..".split(), cwd=builddir) + subprocess.check_call("cmake -D{{ cookiecutter.project_slug }}_BUILD_DOCS=ON -D{{ cookiecutter.project_slug }}_BUILD_TESTING=OFF {% if cookiecutter.python_bindings != 'None' %}-D{{ cookiecutter.project_slug }}_BUILD_PYTHON=OFF{% endif %} ../..".split(), cwd=builddir) subprocess.check_call("cmake --build . --target {{ cookiecutter.project_slug }}-doxygen".split(), cwd=builddir) breathe_projects["{{ cookiecutter.project_slug }}"] = os.path.join(builddir, "doc", "xml") diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index b0017d5f..28b4693c 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -4,7 +4,11 @@ [build-system] requires = [ "scikit-build-core", - "pybind11[global]", + {%- if cookiecutter.python_bindings == "pybind11" %} + "pybind11[global]" + {%- elif cookiecutter.python_bindings == "nanobind" %} + "nanobind[global]" + {%- endif %} ] build-backend = "scikit_build_core.build" diff --git a/{{cookiecutter.project_slug}}/python/{{cookiecutter.__python_module}}/_{{cookiecutter.project_slug}}.cpp b/{{cookiecutter.project_slug}}/python/{{cookiecutter.__python_module}}/_{{cookiecutter.project_slug}}.cpp index 0155e48e..ce4f2ff7 100644 --- a/{{cookiecutter.project_slug}}/python/{{cookiecutter.__python_module}}/_{{cookiecutter.project_slug}}.cpp +++ b/{{cookiecutter.project_slug}}/python/{{cookiecutter.__python_module}}/_{{cookiecutter.project_slug}}.cpp @@ -1,13 +1,25 @@ +{% if cookiecutter.python_bindings == "pybind11" -%} #include #include +{%- elif cookiecutter.python_bindings == "nanobind" %} +#include +{%- endif %} #include "{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}.hpp" +{% if cookiecutter.python_bindings == "pybind11" -%} namespace py = pybind11; +{%- elif cookiecutter.python_bindings == "nanobind" %} +namespace nb = nanobind; +{%- endif %} namespace {{ cookiecutter|modname }} { -PYBIND11_MODULE(_{{ cookiecutter|modname }}, m) +{% if cookiecutter.python_bindings == "pybind11" -%} +PYBIND11_MODULE{%- elif cookiecutter.python_bindings == "nanobind" %} +NB_MODULE +{%- endif -%} +(_{{ cookiecutter|modname }}, m) { m.doc() = "Python Bindings for {{ cookiecutter.project_name }}"; m.def("add_one", &add_one, "Increments an integer value"); From 5c281c10a57de7eb13f08da787d1f898d751f1d7 Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:26:53 +0100 Subject: [PATCH 02/18] chore: bump pre-commit hooks versions --- {{cookiecutter.project_slug}}/.pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml index 622bad48..1ffe0f01 100644 --- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml @@ -2,13 +2,13 @@ repos: {%- if cookiecutter.python_bindings != "None" %} # Run Black - the uncompromising Python code formatter - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 26.3.1 hooks: - id: black-jupyter {% endif %} # Format C++ code with Clang-Format - automatically applying the changes - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v16.0.6 + rev: v22.1.1 hooks: - id: clang-format args: @@ -17,7 +17,7 @@ repos: # Add some general purpose useful hooks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: # Make sure that contained YAML files are well-formed - id: check-yaml @@ -36,7 +36,7 @@ repos: # GitHub Actions Workflow linter - repo: https://github.com/rhysd/actionlint - rev: v1.6.25 + rev: v1.7.11 hooks: - id: actionlint {%- endif %} From 277e115fa26fdf60853575789600afd05e78665b Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:30:06 +0100 Subject: [PATCH 03/18] chore: bump GitHub actions versions --- .github/workflows/ci.yml | 2 +- .github/workflows/pypi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6356c9e..d53a4184 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,7 +75,7 @@ jobs: git config --global user.name "SSC CI Test User" - name: Set up SSH Agent to deploy to test repositories - uses: webfactory/ssh-agent@v0.9.1 + uses: webfactory/ssh-agent@v0.10.0 with: ssh-private-key: | ${{ secrets.GHA_TEST_PRIVATE_KEY }} diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 7896388a..24afd819 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -31,7 +31,7 @@ jobs: git config --global user.name "SSC CI Test User" - name: Set up SSH Agent to deploy to test repositories - uses: webfactory/ssh-agent@v0.9.1 + uses: webfactory/ssh-agent@v0.10.0 with: ssh-private-key: | ${{ secrets.GHA_TEST_PRIVATE_KEY }} From 204f68bf2318dbc64f8203675f2a8b78f3a0d9f6 Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:14:15 +0100 Subject: [PATCH 04/18] refactor: initialize git repository directly with main as branch name --- hooks/post_gen_project.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 812f4efe..c54904fe 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -18,8 +18,7 @@ def __init__(self): def __enter__(self): # Initialize the git repository - subprocess.check_call("git init".split()) - subprocess.check_call("git checkout -b main".split()) + subprocess.check_call("git init -b main".split()) return self def __exit__(self, exc_type, exc_val, exc_tb): From 6db7f1f7c791a869e8143e8fe893c2c74ce69731 Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:33:48 +0100 Subject: [PATCH 05/18] tests: add nanobind option --- tests/test_bake_project.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_bake_project.py b/tests/test_bake_project.py index 575771c5..1b9bb385 100644 --- a/tests/test_bake_project.py +++ b/tests/test_bake_project.py @@ -171,12 +171,13 @@ def test_gitlabci(cookies): @pytest.mark.local +@pytest.mark.parametrize("python_bindings", ("pybind11", "nanobind")) @pytest.mark.parametrize("submodules", ("Yes", "No")) -def test_python(cookies, virtualenv, submodules): +def test_python(cookies, virtualenv, python_bindings, submodules): bake = cookies.bake( extra_context={ 'project_slug': 'my-project', - 'python_bindings': 'pybind11', + 'python_bindings': python_bindings, 'submodules': submodules, } ) From 184ca68e72280e8ee834c85834b10c8eb0ae0537 Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:09:33 +0100 Subject: [PATCH 06/18] ci: run pypi publishing on version tags and allow manual dispatch --- cookiecutter.json | 3 +- tests/test_pypi_release.py | 16 +-- .../.github/workflows/pypi.yml | 136 ++++++++++++++---- {{cookiecutter.project_slug}}/TODO.md | 2 +- 4 files changed, 121 insertions(+), 36 deletions(-) diff --git a/cookiecutter.json b/cookiecutter.json index ec6f5874..ca30c59f 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -21,6 +21,5 @@ "local_extensions.CurrentDateExtension" ], "__python_module": "{{ cookiecutter|modname }}", - "_catch_version": "3.12.0", - "_cibuildwheel_version": "3.3.1" + "_catch_version": "3.13.0" } diff --git a/tests/test_pypi_release.py b/tests/test_pypi_release.py index 8095494e..c24e2677 100644 --- a/tests/test_pypi_release.py +++ b/tests/test_pypi_release.py @@ -32,6 +32,7 @@ def upstream_version(url): # Modify the version in pyproject.toml and commit the change subprocess.check_call("git clone git@github.com:dokempf/test-gha-cookiecutter.git".split()) os.chdir("test-gha-cookiecutter") + subprocess.check_call(["git", "switch", "--track", "origin/pypi_release"]) with open("pyproject.toml", "r") as source: lines = source.readlines() with open("pyproject.toml", "w") as source: @@ -39,20 +40,19 @@ def upstream_version(url): source.write(re.sub(r'version = .*$', 'version = "{}"'.format(str(next_version)), line)) subprocess.check_call("git add pyproject.toml".split()) subprocess.check_call(["git", "commit", "-m", "Bump version in pyproject.toml"]) - subprocess.check_call("git push -f origin main:pypi_release".split()) + subprocess.check_call("git push -f origin pypi_release".split()) time.sleep(2) - # Create the release - this will trigger the PyPI release workflow - repo.create_git_release( - 'v{}'.format(str(next_version)), - 'v{}'.format(str(next_version)), - "Test Release", - target_commitish='pypi_release' + # Create the release tag - this will trigger the PyPI release workflow + branch = repo.get_branch("pypi_release") + + repo.create_git_ref( + ref=f"refs/tags/v{next_version}", + sha=branch.commit.sha, ) time.sleep(2) # Identify the PyPI release workflow - branch = repo.get_branch('pypi_release') workflow = repo.get_workflow("pypi.yml").get_runs()[0] assert workflow.head_sha == branch.commit.sha diff --git a/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml b/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml index 84b758db..b2544392 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml @@ -1,58 +1,144 @@ name: Build Wheels + PyPI deploy on: - release: - types: - - published workflow_dispatch: + inputs: + publish_target: + description: "Choose whether to build only, publish on TestPyPI, or publish on PyPI" + required: true + default: dry-run + type: choice + options: + - dry-run + - testpypi + push: + tags: + - "v*" jobs: build-wheels: - name: Build wheels on ${{ "{{ matrix.os }}" }} - runs-on: ${{ "{{ matrix.os }}" }} + name: Build wheels on {% raw %}${{ matrix.os }}{% endraw %} + runs-on: {% raw %}${{ matrix.os }}{% endraw %} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v6 - with: - submodules: 'recursive' + - name: Checkout repository + uses: actions/checkout@v6 + with: + submodules: 'recursive' + + - name: Set up Python 3.14 + uses: actions/setup-python@v6 + with: + python-version: "3.14" + + - name: Verify release tag matches package version + if: {% raw %}${{ github.event_name == 'push' }}{% endraw %} + run: | + python - <<'PY' + import os + import pathlib + import tomllib + + pyproject = pathlib.Path("pyproject.toml") + version = tomllib.loads(pyproject.read_text())["project"]["version"] + ref = os.environ["GITHUB_REF"] + expected_ref = f"refs/tags/v{version}" + + if ref != expected_ref: + raise SystemExit( + "Publishing requires running this workflow from the " + f"tag {expected_ref}, but GitHub provided {ref!r}." + ) + + print(f"Publishing from {ref}, matching project version {version}.") + PY + + - name: Build wheels + uses: pypa/cibuildwheel@v3 + + - uses: actions/upload-artifact@v7 + with: + name: wheels-{% raw %}${{ matrix.os }}{% endraw %} + path: ./wheelhouse/*.whl + if-no-files-found: error + + build-sdist: + name: Build source distribution + runs-on: ubuntu-latest - - name: Build wheels - uses: pypa/cibuildwheel@v{{ cookiecutter._cibuildwheel_version }} + steps: + - uses: actions/checkout@v6 + with: + submodules: 'recursive' + + - name: Build SDist + run: pipx run build --sdist - - uses: actions/upload-artifact@v5 - with: - path: ./wheelhouse/*.whl + - uses: actions/upload-artifact@v7 + with: + name: sdist + path: dist/*.tar.gz + if-no-files-found: error - upload_testpypi: + upload-testpypi: + name: Publish on TestPyPI + if: {% raw %}${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_target == 'testpypi') }}{% endraw %} needs: [build-wheels] runs-on: ubuntu-latest permissions: id-token: write - if: github.event_name == 'release' && github.event.action == 'published' + environment: + name: testpypi + url: https://test.pypi.org/project/{{ cookiecutter.project_slug }}/ + steps: - - uses: actions/download-artifact@v6 + - name: Download sdist + uses: actions/download-artifact@v8 + with: + name: sdist + merge-multiple: true + path: dist + + - name: Download wheels + uses: actions/download-artifact@v8 with: - name: artifact + pattern: wheels-* + merge-multiple: true path: dist - - uses: pypa/gh-action-pypi-publish@v1.8.11 + - name: Publish distributions to TestPyPI + uses: pypa/gh-action-pypi-publish@v1 with: - repository_url: https://test.pypi.org/legacy/ + repository-url: https://test.pypi.org/legacy/ - upload_pypi: - needs: [build-wheels, upload_testpypi] + upload-pypi: + name: Publish on PyPI + if: {% raw %}${{ github.event_name == 'push' }}{% endraw %} + needs: [build-wheels, upload-testpypi] runs-on: ubuntu-latest permissions: id-token: write - if: github.event_name == 'release' && github.event.action == 'published' + environment: + name: pypi + url: https://pypi.org/project/{{ cookiecutter.project_slug }}/ + steps: - - uses: actions/download-artifact@v6 + - name: Download sdist + uses: actions/download-artifact@v8 + with: + name: sdist + merge-multiple: true + path: dist + + - name: Download wheels + uses: actions/download-artifact@v8 with: - name: artifact + pattern: wheels-* + merge-multiple: true path: dist - - uses: pypa/gh-action-pypi-publish@release/v1.8.11 + - uses: pypa/gh-action-pypi-publish@release/v1.13.0 diff --git a/{{cookiecutter.project_slug}}/TODO.md b/{{cookiecutter.project_slug}}/TODO.md index 29310458..539bea70 100644 --- a/{{cookiecutter.project_slug}}/TODO.md +++ b/{{cookiecutter.project_slug}}/TODO.md @@ -51,7 +51,7 @@ The following tasks need to be done to get a fully working project: * Owner: `{{ cookiecutter|username }}` * Repository name: `{{ cookiecutter|remote_slug }}` * Workflow name: `pypi.yml` - * Environment name: not required + * Environment name: pypi [testpypi] {%- endif %} {%- if cookiecutter.codecovio == "Yes" %} * Enable the integration with `codecov.io` by heading to the [Codecov.io Website](https://codecov.io), From 218fb26251d065c4e05fb600ec6b7444a9c1c869 Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:57:03 +0100 Subject: [PATCH 07/18] fix: use tomlkit to modify only the pyproject version, not the cmake version --- requirements-dev.txt | 1 + tests/test_pypi_release.py | 34 ++++++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 215658b1..e425b479 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,3 +14,4 @@ requests ruamel.yaml sphinx sphinx_rtd_theme +tomlkit diff --git a/tests/test_pypi_release.py b/tests/test_pypi_release.py index c24e2677..70f72b65 100644 --- a/tests/test_pypi_release.py +++ b/tests/test_pypi_release.py @@ -5,6 +5,8 @@ import requests import subprocess import time +import tomlkit + from packaging import version @@ -22,22 +24,34 @@ def upstream_version(url): # Construct a version, by finding the maximum version across Github, PyPI and TestPyPI and increasing that gh = github.Github(os.getenv("GH_API_ACCESS_TOKEN")) repo = gh.get_repo('dokempf/test-gha-cookiecutter') + + # Modify the version in pyproject.toml and commit the change + subprocess.check_call("git clone git@github.com:dokempf/test-gha-cookiecutter.git".split()) + os.chdir("test-gha-cookiecutter") + subprocess.check_call(["git", "switch", "--track", "origin/pypi_release"]) + + # Parse the pyproject.toml file to get the current version on the branch + with open("pyproject.toml", "r") as f: + data = tomlkit.parse(f.read()) + branch_version = version.parse(data["project"]["version"]) + + # Identify the maximum version across PyPI, TestPyPI, Github and the branch and increase that by one patch version current_version = max([ upstream_version('https://pypi.org/pypi/testghacookiecutter/json'), upstream_version('https://test.pypi.org/pypi/testghacookiecutter/json'), - version.parse(repo.get_latest_release().title[1:]) + version.parse(repo.get_latest_release().title[1:]), + branch_version ]) + + # Increase the version by one next_version = version.Version('{}.{}.{}'.format(current_version.major, current_version.minor, current_version.micro + 1)) - # Modify the version in pyproject.toml and commit the change - subprocess.check_call("git clone git@github.com:dokempf/test-gha-cookiecutter.git".split()) - os.chdir("test-gha-cookiecutter") - subprocess.check_call(["git", "switch", "--track", "origin/pypi_release"]) - with open("pyproject.toml", "r") as source: - lines = source.readlines() - with open("pyproject.toml", "w") as source: - for line in lines: - source.write(re.sub(r'version = .*$', 'version = "{}"'.format(str(next_version)), line)) + # Update the version in pyproject.toml + data["project"]["version"] = str(next_version) + with open("pyproject.toml", "w") as f: + f.write(tomlkit.dumps(data)) + + # Commit the change and push it to the remote branch subprocess.check_call("git add pyproject.toml".split()) subprocess.check_call(["git", "commit", "-m", "Bump version in pyproject.toml"]) subprocess.check_call("git push -f origin pypi_release".split()) From e085ab10402b31d3f1e2d31bb518a702733ab664 Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Tue, 19 May 2026 12:46:00 +0200 Subject: [PATCH 08/18] fix: do not switch to pypi_release branch, instead make change on main and push to pypi_release --- tests/test_pypi_release.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_pypi_release.py b/tests/test_pypi_release.py index 70f72b65..72fa2fc1 100644 --- a/tests/test_pypi_release.py +++ b/tests/test_pypi_release.py @@ -28,7 +28,6 @@ def upstream_version(url): # Modify the version in pyproject.toml and commit the change subprocess.check_call("git clone git@github.com:dokempf/test-gha-cookiecutter.git".split()) os.chdir("test-gha-cookiecutter") - subprocess.check_call(["git", "switch", "--track", "origin/pypi_release"]) # Parse the pyproject.toml file to get the current version on the branch with open("pyproject.toml", "r") as f: @@ -54,7 +53,7 @@ def upstream_version(url): # Commit the change and push it to the remote branch subprocess.check_call("git add pyproject.toml".split()) subprocess.check_call(["git", "commit", "-m", "Bump version in pyproject.toml"]) - subprocess.check_call("git push -f origin pypi_release".split()) + subprocess.check_call("git push -f origin HEAD:pypi_release".split()) time.sleep(2) # Create the release tag - this will trigger the PyPI release workflow From 4bc527e09d221e48181d6c62a2dd8375ccb13d8c Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Tue, 19 May 2026 13:03:45 +0200 Subject: [PATCH 09/18] fix: consider also existing tags for max version --- tests/test_pypi_release.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/test_pypi_release.py b/tests/test_pypi_release.py index 72fa2fc1..67a6736d 100644 --- a/tests/test_pypi_release.py +++ b/tests/test_pypi_release.py @@ -10,6 +10,20 @@ from packaging import version +def github_tag_versions(repo): + versions = [] + + for tag in repo.get_tags(): + if not tag.name.startswith("v"): + continue + + try: + versions.append(version.parse(tag.name[1:])) + except version.InvalidVersion: + pass + + return versions + @pytest.mark.pypi @pytest.mark.timeout(1800) def test_pypi_deploy(): @@ -39,7 +53,8 @@ def upstream_version(url): upstream_version('https://pypi.org/pypi/testghacookiecutter/json'), upstream_version('https://test.pypi.org/pypi/testghacookiecutter/json'), version.parse(repo.get_latest_release().title[1:]), - branch_version + branch_version, + *github_tag_versions(repo) ]) # Increase the version by one From 4e86f1508e8fb354fcef9799b9ee16c1d54d5dda Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Tue, 19 May 2026 13:15:52 +0200 Subject: [PATCH 10/18] ci: define pypa/cibuildwheel version in cookiecutter.json --- cookiecutter.json | 3 ++- {{cookiecutter.project_slug}}/.github/workflows/pypi.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cookiecutter.json b/cookiecutter.json index ca30c59f..902c01a9 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -21,5 +21,6 @@ "local_extensions.CurrentDateExtension" ], "__python_module": "{{ cookiecutter|modname }}", - "_catch_version": "3.13.0" + "_catch_version": "3.13.0", + "_cibuildwheel_version": "3.4.1" } diff --git a/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml b/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml index b2544392..7be76c62 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml @@ -58,7 +58,7 @@ jobs: PY - name: Build wheels - uses: pypa/cibuildwheel@v3 + uses: pypa/cibuildwheel@v{{ cookiecutter._cibuildwheel_version }} - uses: actions/upload-artifact@v7 with: From 79b31f67bf6eb6f755c6ee49fae9a6c3c121459d Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Tue, 19 May 2026 13:24:36 +0200 Subject: [PATCH 11/18] fix(ci): use python shell to prevent error on windows runners --- {{cookiecutter.project_slug}}/.github/workflows/pypi.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml b/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml index 7be76c62..d5fdf622 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml @@ -37,8 +37,8 @@ jobs: - name: Verify release tag matches package version if: {% raw %}${{ github.event_name == 'push' }}{% endraw %} + shell: python run: | - python - <<'PY' import os import pathlib import tomllib @@ -55,7 +55,6 @@ jobs: ) print(f"Publishing from {ref}, matching project version {version}.") - PY - name: Build wheels uses: pypa/cibuildwheel@v{{ cookiecutter._cibuildwheel_version }} From b9298713cee21ce2679daea9a7dc73ef21bf002d Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Tue, 19 May 2026 13:31:04 +0200 Subject: [PATCH 12/18] fix(ci): uploat-testpypi and upload-pypi need build-sdist --- {{cookiecutter.project_slug}}/.github/workflows/pypi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml b/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml index d5fdf622..9a993942 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml @@ -86,7 +86,7 @@ jobs: upload-testpypi: name: Publish on TestPyPI if: {% raw %}${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_target == 'testpypi') }}{% endraw %} - needs: [build-wheels] + needs: [build-wheels, build-sdist] runs-on: ubuntu-latest permissions: id-token: write @@ -117,7 +117,7 @@ jobs: upload-pypi: name: Publish on PyPI if: {% raw %}${{ github.event_name == 'push' }}{% endraw %} - needs: [build-wheels, upload-testpypi] + needs: [build-wheels, build-sdist, upload-testpypi] runs-on: ubuntu-latest permissions: id-token: write From 5220baffc0bb70207af4bbfda4a3328c04594846 Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Tue, 19 May 2026 13:53:55 +0200 Subject: [PATCH 13/18] fix: give gh more time to start workflow + don't assume get_runs()[0] is the correct run --- tests/test_deploy_bake.py | 32 ++++++++++++++++++-------------- tests/test_pypi_release.py | 36 ++++++++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/tests/test_deploy_bake.py b/tests/test_deploy_bake.py index 1ee43336..8207dd89 100644 --- a/tests/test_deploy_bake.py +++ b/tests/test_deploy_bake.py @@ -33,9 +33,20 @@ def test_push_remote(cookies): subprocess.check_call("git push -f gitlab main".split()) +def find_github_workflow_run(repo, workflow_name, sha, branch="main", event="push"): + workflow = repo.get_workflow(workflow_name) + + for _ in range(30): + for run in workflow.get_runs(branch=branch): + if run.head_sha == sha and run.event == event: + return run + + time.sleep(10) + + pytest.fail(f"No {workflow_name} run found for {branch} at {sha}") + @pytest.mark.integrations -@pytest.mark.flaky(max_runs=3, min_passes=1, rerun_filter=wait_five_seconds) -@pytest.mark.timeout(300) +@pytest.mark.timeout(600) def test_github_actions_ci_on_deployed_bake(): # Authenticate with the Github API gh = github.Github(os.getenv("GH_API_ACCESS_TOKEN")) @@ -43,21 +54,14 @@ def test_github_actions_ci_on_deployed_bake(): # Identify the correct workflow repo = gh.get_repo('dokempf/test-gha-cookiecutter') branch = repo.get_branch('main') - workflow = repo.get_workflow("ci.yml").get_runs()[0] + workflow = find_github_workflow_run(repo, "ci.yml", branch.commit.sha) assert workflow.head_sha == branch.commit.sha - def check_workflow(name): - # Poll the workflow status - workflow = repo.get_workflow(name).get_runs()[0] - while workflow.status != 'completed': - # We poll at a relatively large interval to avoid running against the Github API - # limitations in times of heavy development activities on the cookiecutter. - time.sleep(30) - workflow = repo.get_workflow(name).get_runs()[0] - - assert workflow.conclusion == 'success' + while workflow.status != "completed": + time.sleep(30) + workflow = repo.get_workflow_run(workflow.id) - check_workflow("ci.yml") + assert workflow.conclusion == "success" @pytest.mark.integrations diff --git a/tests/test_pypi_release.py b/tests/test_pypi_release.py index 67a6736d..1b5f679a 100644 --- a/tests/test_pypi_release.py +++ b/tests/test_pypi_release.py @@ -68,6 +68,9 @@ def upstream_version(url): # Commit the change and push it to the remote branch subprocess.check_call("git add pyproject.toml".split()) subprocess.check_call(["git", "commit", "-m", "Bump version in pyproject.toml"]) + + commit_sha = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True,).strip() + subprocess.check_call("git push -f origin HEAD:pypi_release".split()) time.sleep(2) @@ -76,19 +79,32 @@ def upstream_version(url): repo.create_git_ref( ref=f"refs/tags/v{next_version}", - sha=branch.commit.sha, + sha=commit_sha, ) - time.sleep(2) - # Identify the PyPI release workflow - workflow = repo.get_workflow("pypi.yml").get_runs()[0] - assert workflow.head_sha == branch.commit.sha + + # Wait until the PyPI release workflow triggered by this tag appears + pypi_workflow = repo.get_workflow("pypi.yml") + + workflow = None + for _ in range(30): + runs = pypi_workflow.get_runs() + + for run in runs: + if run.head_sha == commit_sha: + workflow = run + break + + if workflow is not None: + break + + time.sleep(10) + + assert workflow is not None, f"No pypi.yml workflow run found for commit {commit_sha}" # Poll the workflow status - while workflow.status != 'completed': - # We poll at a relatively large interval to avoid running against the Github API - # limitations in times of heavy development activities on the cookiecutter. + while workflow.status != "completed": time.sleep(30) - workflow = repo.get_workflow("pypi.yml").get_runs()[0] + workflow = repo.get_workflow_run(workflow.id) - assert workflow.conclusion == 'success' + assert workflow.conclusion == "success" From 6fdd2ec67becfc238d537114d1b3237e7589964a Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Tue, 19 May 2026 14:12:09 +0200 Subject: [PATCH 14/18] fix(ci): use pypa/gh-action-pypi-publish@release/v1 --- {{cookiecutter.project_slug}}/.github/workflows/pypi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml b/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml index 9a993942..0f3d0d20 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml @@ -110,7 +110,7 @@ jobs: path: dist - name: Publish distributions to TestPyPI - uses: pypa/gh-action-pypi-publish@v1 + uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ @@ -140,4 +140,4 @@ jobs: merge-multiple: true path: dist - - uses: pypa/gh-action-pypi-publish@release/v1.13.0 + - uses: pypa/gh-action-pypi-publish@release/v1 From 01036271c0692984f34a0e62de8c3dc1ff726c15 Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Tue, 19 May 2026 15:18:00 +0200 Subject: [PATCH 15/18] ci: remove GH actions environment --- {{cookiecutter.project_slug}}/.github/workflows/pypi.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml b/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml index 0f3d0d20..f3e73b8a 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/pypi.yml @@ -90,9 +90,6 @@ jobs: runs-on: ubuntu-latest permissions: id-token: write - environment: - name: testpypi - url: https://test.pypi.org/project/{{ cookiecutter.project_slug }}/ steps: - name: Download sdist @@ -121,9 +118,6 @@ jobs: runs-on: ubuntu-latest permissions: id-token: write - environment: - name: pypi - url: https://pypi.org/project/{{ cookiecutter.project_slug }}/ steps: - name: Download sdist From 7efab77ec1e43b0d6d74bed4387e420cae373845 Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Tue, 19 May 2026 15:26:40 +0200 Subject: [PATCH 16/18] test: remove unused import --- tests/test_pypi_release.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_pypi_release.py b/tests/test_pypi_release.py index 1b5f679a..49c7e98d 100644 --- a/tests/test_pypi_release.py +++ b/tests/test_pypi_release.py @@ -1,6 +1,5 @@ import github import os -import re import pytest import requests import subprocess From 402a5207d55ef31aaddf559f6df532b621db04a3 Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Tue, 19 May 2026 16:02:37 +0200 Subject: [PATCH 17/18] fix: PyPI incorrect classifier --- {{cookiecutter.project_slug}}/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index 28b4693c..1a899bc2 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -38,7 +38,7 @@ license = "LGPL-3.0-only" {%- endif %} classifiers = [ - "Programming Language :: Python :: C++", + "Programming Language :: C++", "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] From 2dcbae2f365bdc9c4f74821e6d1fd26a77a58574 Mon Sep 17 00:00:00 2001 From: Thomas Isensee <26852629+thomasisensee@users.noreply.github.com> Date: Thu, 18 Jun 2026 09:47:26 +0200 Subject: [PATCH 18/18] chore: bump pre-commit hooks versions --- .pre-commit-config.yaml | 2 +- {{cookiecutter.project_slug}}/.pre-commit-config.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9702499..f0bab2d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,6 +16,6 @@ repos: # GitHub Actions Workflow linter - repo: https://github.com/rhysd/actionlint - rev: v1.7.11 + rev: v1.7.12 hooks: - id: actionlint diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml index 1ffe0f01..f604b47f 100644 --- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml @@ -2,13 +2,13 @@ repos: {%- if cookiecutter.python_bindings != "None" %} # Run Black - the uncompromising Python code formatter - repo: https://github.com/psf/black - rev: 26.3.1 + rev: 26.5.1 hooks: - id: black-jupyter {% endif %} # Format C++ code with Clang-Format - automatically applying the changes - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v22.1.1 + rev: v22.1.5 hooks: - id: clang-format args: @@ -36,7 +36,7 @@ repos: # GitHub Actions Workflow linter - repo: https://github.com/rhysd/actionlint - rev: v1.7.11 + rev: v1.7.12 hooks: - id: actionlint {%- endif %}