From e2bdf1a20d8e85b8a9ed1831d49a2a1f00dac229 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Fri, 28 Nov 2025 03:33:30 -0500 Subject: [PATCH 01/43] chore: replace outdated env var with newer standard prefixed version --- .github/workflows/sync-demos.yml | 1 - noxfile.py | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sync-demos.yml b/.github/workflows/sync-demos.yml index abeab83..4c89862 100644 --- a/.github/workflows/sync-demos.yml +++ b/.github/workflows/sync-demos.yml @@ -5,7 +5,6 @@ on: - develop env: - COOKIECUTTER_ROBUST_PYTHON_PROJECT_DEMOS_FOLDER: ${{ github.workspace }} COOKIECUTTER_ROBUST_PYTHON__DEMOS_CACHE_FOLDER: ${{ github.workspace }} COOKIECUTTER_ROBUST_PYTHON__APP_AUTHOR: ${{ github.repository_owner }} ROBUST_PYTHON_DEMO__APP_AUTHOR: ${{ github.repository_owner }} diff --git a/noxfile.py b/noxfile.py index 33d7d3d..a85cf7e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -46,16 +46,16 @@ ) ).resolve() -DEFAULT_PROJECT_DEMOS_FOLDER = COOKIECUTTER_ROBUST_PYTHON_CACHE_FOLDER / "project_demos" -PROJECT_DEMOS_FOLDER: Path = Path(os.getenv( - "COOKIECUTTER_ROBUST_PYTHON_PROJECT_DEMOS_FOLDER", default=DEFAULT_PROJECT_DEMOS_FOLDER +DEFAULT_DEMOS_CACHE_FOLDER = COOKIECUTTER_ROBUST_PYTHON_CACHE_FOLDER / "project_demos" +DEMOS_CACHE_FOLDER: Path = Path(os.getenv( + "COOKIECUTTER_ROBUST_PYTHON__DEMOS_CACHE_FOLDER", default=DEFAULT_DEMOS_CACHE_FOLDER )).resolve() DEFAULT_DEMO_NAME: str = "robust-python-demo" -DEMO_ROOT_FOLDER: Path = PROJECT_DEMOS_FOLDER / DEFAULT_DEMO_NAME +DEMO_ROOT_FOLDER: Path = DEMOS_CACHE_FOLDER / DEFAULT_DEMO_NAME GENERATE_DEMO_SCRIPT: Path = SCRIPTS_FOLDER / "generate-demo.py" GENERATE_DEMO_OPTIONS: tuple[str, ...] = ( - *("--demos-cache-folder", PROJECT_DEMOS_FOLDER), + *("--demos-cache-folder", DEMOS_CACHE_FOLDER), ) LINT_FROM_DEMO_SCRIPT: Path = SCRIPTS_FOLDER / "lint-from-demo.py" @@ -118,7 +118,7 @@ def clear_cache(session: Session) -> None: Not commonly used, but sometimes permissions might get messed up if exiting mid-build and such. """ session.log("Clearing cache of generated project demos...") - shutil.rmtree(PROJECT_DEMOS_FOLDER, ignore_errors=True) + shutil.rmtree(DEMOS_CACHE_FOLDER, ignore_errors=True) session.log("Cache cleared.") From 51cc71ac40fd7e2f87a63d0399b87c6c713d3705 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Fri, 28 Nov 2025 10:45:46 -0500 Subject: [PATCH 02/43] feat: break out demo updates into its own reusable local github action workflow and add in additional syncing for demos with feature branch PR's and general push workflow on develop --- .github/workflows/merge-demo-feature.yml | 24 ++++++++++++ .github/workflows/sync-demos.yml | 26 ++----------- .github/workflows/update-demo.yml | 36 ++++++++++++++++++ noxfile.py | 26 ++++++++++++- scripts/merge-demo-feature.py | 47 ++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/merge-demo-feature.yml create mode 100644 .github/workflows/update-demo.yml create mode 100644 scripts/merge-demo-feature.py diff --git a/.github/workflows/merge-demo-feature.yml b/.github/workflows/merge-demo-feature.yml new file mode 100644 index 0000000..a4367b2 --- /dev/null +++ b/.github/workflows/merge-demo-feature.yml @@ -0,0 +1,24 @@ +name: merge-demo-feature.yml +on: + push: + branches: + - develop + +jobs: + merge-demo-feature: + name: Merge Demo Feature + runs-on: ubuntu-latest + strategy: + matrix: + demo_name: + - "robust-python-demo" + - "robust-maturin-demo" + steps: + - name: Sync Demo + uses: "./.github/workflows/update-demo.yml" + with: + demo_name: ${{ matrix.demo_name }} + + - name: Merge Demo Feature PR into Develop + working-directory: "${{ github.workspace }}/${{ matrix.demo_name }}" + run: "uvx nox -s merge-demo-feature(${{ matrix.demo_name }}) -- ${{ github.head_ref }}" diff --git a/.github/workflows/sync-demos.yml b/.github/workflows/sync-demos.yml index 4c89862..5d9d84d 100644 --- a/.github/workflows/sync-demos.yml +++ b/.github/workflows/sync-demos.yml @@ -21,27 +21,7 @@ jobs: - "robust-python-demo" - "robust-maturin-demo" steps: - - name: Checkout Template - uses: actions/checkout@v4 + - name: Sync Demo + uses: "./.github/workflows/update-demo.yml" with: - repository: ${{ github.repository }} - path: cookiecutter-robust-python - - - name: Checkout Demo - uses: actions/checkout@v4 - with: - repository: "${{ github.repository_owner }}/${{ matrix.demo_name }}" - path: ${{ matrix.demo_name }} - ref: develop - - - name: Set up uv - uses: astral-sh/setup-uv@v6 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Update Demo - working-directory: "${{ github.workspace }}/cookiecutter-robust-python" - run: "uvx nox -s 'update-demo(${{ matrix.demo_name }})' -- --branch-override ${{ github.head_ref }}" + demo_name: ${{ matrix.demo_name }} diff --git a/.github/workflows/update-demo.yml b/.github/workflows/update-demo.yml new file mode 100644 index 0000000..1f40157 --- /dev/null +++ b/.github/workflows/update-demo.yml @@ -0,0 +1,36 @@ +name: update-demo.yml +on: + workflow_call: + inputs: + demo_name: + required: true + type: string + +jobs: + update-demo: + runs-on: ubuntu-latest + steps: + - name: Checkout Template + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }} + path: cookiecutter-robust-python + + - name: Checkout Demo + uses: actions/checkout@v4 + with: + repository: "${{ github.repository_owner }}/${{ inputs.demo_name }}" + path: ${{ inputs.demo_name }} + ref: develop + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: ".github/workflows/.python-version" + + - name: Update Demo + working-directory: "${{ github.workspace }}/cookiecutter-robust-python" + run: "uvx nox -s 'update-demo(${{ inputs.demo_name }})' -- --branch-override ${{ github.head_ref }}" diff --git a/noxfile.py b/noxfile.py index a85cf7e..bf3987f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -17,6 +17,8 @@ from nox.command import CommandFailed from nox.sessions import Session +from scripts.util import get_current_branch + nox.options.default_venv_backend = "uv" @@ -68,6 +70,9 @@ *("--max-python-version", "3.14") ) +MERGE_DEMO_FEATURE_SCRIPT: Path = SCRIPTS_FOLDER / "merge-demo-feature.py" +MERGE_DEMO_FEATURE_OPTIONS: tuple[str, ...] = GENERATE_DEMO_OPTIONS + @dataclass class RepoMetadata: @@ -200,7 +205,26 @@ def update_demo(session: Session, demo: RepoMetadata) -> None: args.extend(session.posargs) demo_env: dict[str, Any] = {f"ROBUST_DEMO__{key.upper()}": value for key, value in asdict(demo).items()} - session.run("python", UPDATE_DEMO_SCRIPT, *args, env=demo_env) + session.run("uv", "run", UPDATE_DEMO_SCRIPT, *args, env=demo_env) + + +@nox.parametrize( + arg_names="demo", + arg_values_list=[PYTHON_DEMO, MATURIN_DEMO], + ids=["robust-python-demo", "robust-maturin-demo"] +) +@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="merge-demo-feature") +def merge_demo_feature(session: Session, demo: RepoMetadata) -> None: + """Automates merging the current feature branch to develop in all templates. + + Assumes that all PR's already exist. + """ + args: list[str] = [*MERGE_DEMO_FEATURE_OPTIONS] + if "maturin" in demo.app_name: + args.append("--add-rust-extension") + if session.posargs: + args.extend(session.posargs) + session.run("uv", "run", MERGE_DEMO_FEATURE_SCRIPT, *args) @nox.session(python=False, name="release-template") diff --git a/scripts/merge-demo-feature.py b/scripts/merge-demo-feature.py new file mode 100644 index 0000000..c48a395 --- /dev/null +++ b/scripts/merge-demo-feature.py @@ -0,0 +1,47 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "cookiecutter", +# "cruft", +# "python-dotenv", +# "typer", +# ] +# /// +import subprocess +from pathlib import Path +from typing import Annotated + +import typer +from cookiecutter.utils import work_in + +from util import DEMO +from util import FolderOption +from util import get_demo_name +from util import gh + + +cli: typer.Typer = typer.Typer() + + +@cli.callback(invoke_without_command=True) +def merge_demo_feature( + branch: str, + demos_cache_folder: Annotated[Path, FolderOption("--demos-cache-folder", "-c")], + add_rust_extension: Annotated[bool, typer.Option("--add-rust-extension", "-r")] = False +) -> None: + """Searches for the given demo feature branch's PR and merges it if ready.""" + demo_name: str = get_demo_name(add_rust_extension=add_rust_extension) + demo_path: Path = demos_cache_folder / demo_name + with work_in(demo_path): + pr_number_query: subprocess.CompletedProcess = gh( + "pr", "list", "--head", branch, "--base", DEMO.develop_branch, "--json", "number", "--jq", "'.[0].number'" + ) + pr_number: str = pr_number_query.stdout.strip() + if pr_number == "": + raise ValueError("Failed to find an existing PR from {} to {DEMO.develop_branch}") + + gh("pr", "merge", pr_number, "--auto", "--delete-branch") + + +if __name__ == "__main__": + cli() From 85d5b9ed390c1e780193cd0ca887ffb03cbd915a Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Fri, 28 Nov 2025 10:57:01 -0500 Subject: [PATCH 03/43] feat: move initial template checkout back into the primary workflows to ensure that the reusable workflow exists and is checked out prior to attempting to reference it --- .github/workflows/merge-demo-feature.yml | 12 ++++++++++++ .github/workflows/sync-demos.yml | 14 +++++++------- .github/workflows/update-demo.yml | 12 ++++++------ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.github/workflows/merge-demo-feature.yml b/.github/workflows/merge-demo-feature.yml index a4367b2..62ee043 100644 --- a/.github/workflows/merge-demo-feature.yml +++ b/.github/workflows/merge-demo-feature.yml @@ -4,6 +4,12 @@ on: branches: - develop +env: + COOKIECUTTER_ROBUST_PYTHON__DEMOS_CACHE_FOLDER: ${{ github.workspace }} + COOKIECUTTER_ROBUST_PYTHON__APP_AUTHOR: ${{ github.repository_owner }} + ROBUST_PYTHON_DEMO__APP_AUTHOR: ${{ github.repository_owner }} + ROBUST_MATURIN_DEMO__APP_AUTHOR: ${{ github.repository_owner }} + jobs: merge-demo-feature: name: Merge Demo Feature @@ -14,6 +20,12 @@ jobs: - "robust-python-demo" - "robust-maturin-demo" steps: + - name: Checkout Template + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }} + path: cookiecutter-robust-python + - name: Sync Demo uses: "./.github/workflows/update-demo.yml" with: diff --git a/.github/workflows/sync-demos.yml b/.github/workflows/sync-demos.yml index 5d9d84d..0676c9d 100644 --- a/.github/workflows/sync-demos.yml +++ b/.github/workflows/sync-demos.yml @@ -4,12 +4,6 @@ on: branches: - develop -env: - COOKIECUTTER_ROBUST_PYTHON__DEMOS_CACHE_FOLDER: ${{ github.workspace }} - COOKIECUTTER_ROBUST_PYTHON__APP_AUTHOR: ${{ github.repository_owner }} - ROBUST_PYTHON_DEMO__APP_AUTHOR: ${{ github.repository_owner }} - ROBUST_MATURIN_DEMO__APP_AUTHOR: ${{ github.repository_owner }} - jobs: update-demo: name: Update Demo @@ -21,7 +15,13 @@ jobs: - "robust-python-demo" - "robust-maturin-demo" steps: - - name: Sync Demo + - name: Checkout Template + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }} + path: cookiecutter-robust-python + + - name: Update Demo uses: "./.github/workflows/update-demo.yml" with: demo_name: ${{ matrix.demo_name }} diff --git a/.github/workflows/update-demo.yml b/.github/workflows/update-demo.yml index 1f40157..9b971ed 100644 --- a/.github/workflows/update-demo.yml +++ b/.github/workflows/update-demo.yml @@ -6,16 +6,16 @@ on: required: true type: string +env: + COOKIECUTTER_ROBUST_PYTHON__DEMOS_CACHE_FOLDER: ${{ github.workspace }} + COOKIECUTTER_ROBUST_PYTHON__APP_AUTHOR: ${{ github.repository_owner }} + ROBUST_PYTHON_DEMO__APP_AUTHOR: ${{ github.repository_owner }} + ROBUST_MATURIN_DEMO__APP_AUTHOR: ${{ github.repository_owner }} + jobs: update-demo: runs-on: ubuntu-latest steps: - - name: Checkout Template - uses: actions/checkout@v4 - with: - repository: ${{ github.repository }} - path: cookiecutter-robust-python - - name: Checkout Demo uses: actions/checkout@v4 with: From f98357d47df2805019b6c4769c14755a34a58fc5 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Fri, 28 Nov 2025 11:34:19 -0500 Subject: [PATCH 04/43] fix: add absolute prefix to custom checkout locations in hopes of fixing issue with reusable action not being found in CICD --- .github/workflows/merge-demo-feature.yml | 2 +- .github/workflows/sync-demos.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/merge-demo-feature.yml b/.github/workflows/merge-demo-feature.yml index 62ee043..ab32a0c 100644 --- a/.github/workflows/merge-demo-feature.yml +++ b/.github/workflows/merge-demo-feature.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v4 with: repository: ${{ github.repository }} - path: cookiecutter-robust-python + path: "${{ github.workspace }}/cookiecutter-robust-python" - name: Sync Demo uses: "./.github/workflows/update-demo.yml" diff --git a/.github/workflows/sync-demos.yml b/.github/workflows/sync-demos.yml index 0676c9d..54a79e6 100644 --- a/.github/workflows/sync-demos.yml +++ b/.github/workflows/sync-demos.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v4 with: repository: ${{ github.repository }} - path: cookiecutter-robust-python + path: "${{ github.workspace }}/cookiecutter-robust-python" - name: Update Demo uses: "./.github/workflows/update-demo.yml" From e7c3c70d1328f972405380e187a18a7791eaf332 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Fri, 28 Nov 2025 11:40:24 -0500 Subject: [PATCH 05/43] fix: update the location of where the update-demo.yml reusable workflow is searched for to be in the subdirectory that is actually being checked out into --- .github/workflows/merge-demo-feature.yml | 2 +- .github/workflows/sync-demos.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/merge-demo-feature.yml b/.github/workflows/merge-demo-feature.yml index ab32a0c..6ab3f83 100644 --- a/.github/workflows/merge-demo-feature.yml +++ b/.github/workflows/merge-demo-feature.yml @@ -27,7 +27,7 @@ jobs: path: "${{ github.workspace }}/cookiecutter-robust-python" - name: Sync Demo - uses: "./.github/workflows/update-demo.yml" + uses: "${{ github.workspace }}/cookiecutter-robust-python/.github/workflows/update-demo.yml" with: demo_name: ${{ matrix.demo_name }} diff --git a/.github/workflows/sync-demos.yml b/.github/workflows/sync-demos.yml index 54a79e6..239b49c 100644 --- a/.github/workflows/sync-demos.yml +++ b/.github/workflows/sync-demos.yml @@ -22,6 +22,6 @@ jobs: path: "${{ github.workspace }}/cookiecutter-robust-python" - name: Update Demo - uses: "./.github/workflows/update-demo.yml" + uses: "${{ github.workspace }}/cookiecutter-robust-python/.github/workflows/update-demo.yml" with: demo_name: ${{ matrix.demo_name }} From c8ae22dc82a571a713664b0dfb0d961357d91efd Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Fri, 28 Nov 2025 17:14:17 -0500 Subject: [PATCH 06/43] fix: adjust formatting to avoid initial lint error --- {{cookiecutter.project_name}}/scripts/setup-release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/{{cookiecutter.project_name}}/scripts/setup-release.py b/{{cookiecutter.project_name}}/scripts/setup-release.py index 258728b..2413064 100644 --- a/{{cookiecutter.project_name}}/scripts/setup-release.py +++ b/{{cookiecutter.project_name}}/scripts/setup-release.py @@ -77,7 +77,7 @@ def _rollback_release(version: str) -> None: commands: list[list[str]] = [ ["git", "checkout", "develop"], ["git", "checkout", "."], - ["git", "branch", "-D", f"release/{version}"] + ["git", "branch", "-D", f"release/{version}"], ] for command in commands: From be58c21e401872ce0d915dd57bcb988131f03207 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Fri, 28 Nov 2025 17:15:00 -0500 Subject: [PATCH 07/43] fix: remove faulty import --- noxfile.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index bf3987f..b926add 100644 --- a/noxfile.py +++ b/noxfile.py @@ -17,8 +17,6 @@ from nox.command import CommandFailed from nox.sessions import Session -from scripts.util import get_current_branch - nox.options.default_venv_backend = "uv" From a7a37768292366a35772ab1aaf8a1454e534c990 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sat, 29 Nov 2025 22:22:55 -0500 Subject: [PATCH 08/43] feat: add initial implementation of the calendar version release cicd --- .cz.toml | 4 + .github/workflows/.python-version | 1 + .github/workflows/prepare-release.yml | 52 +++++++++ .github/workflows/release-template.yml | 147 +++++++++++++++++++++++++ noxfile.py | 106 +++++++++++++----- pyproject.toml | 2 +- scripts/bump-version.py | 49 +++++++++ scripts/get-release-notes.py | 48 ++++++++ scripts/util.py | 109 ++++++++++++++++++ 9 files changed, 487 insertions(+), 31 deletions(-) create mode 100644 .cz.toml create mode 100644 .github/workflows/.python-version create mode 100644 .github/workflows/prepare-release.yml create mode 100644 .github/workflows/release-template.yml create mode 100644 scripts/bump-version.py create mode 100644 scripts/get-release-notes.py diff --git a/.cz.toml b/.cz.toml new file mode 100644 index 0000000..864ff52 --- /dev/null +++ b/.cz.toml @@ -0,0 +1,4 @@ +[tool.commitizen] +tag_format = "v$version" +version_provider = "pep621" +update_changelog_on_bump = true diff --git a/.github/workflows/.python-version b/.github/workflows/.python-version new file mode 100644 index 0000000..c8cfe39 --- /dev/null +++ b/.github/workflows/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..2217f10 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,52 @@ +name: Prepare Release + +on: + push: + branches: + - "release/*" + +permissions: + contents: write + +jobs: + prepare-release: + name: Prepare Release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: .github/workflows/.python-version + + - name: Get Current Version + id: current_version + run: echo "CURRENT_VERSION=$(uvx --from commitizen cz version -p)" >> $GITHUB_OUTPUT + + - name: Get New Release Version + id: new_version + run: echo "NEW_VERSION=${GITHUB_REF_NAME#release/}" >> $GITHUB_OUTPUT + + - name: Bump Version + if: ${{ steps.current_version.outputs.CURRENT_VERSION != steps.new_version.outputs.NEW_VERSION }} + run: uvx nox -s bump-version ${{ steps.new_version.outputs.NEW_VERSION }} + + - name: Get Release Notes + run: uvx nox -s get-release-notes -- ${{ github.workspace }}-CHANGELOG.md + + - name: Create Release Draft + uses: softprops/action-gh-release@v2 + with: + body_path: ${{ github.workspace }}-CHANGELOG.md + draft: true + tag_name: v${{ steps.new_version.outputs.NEW_VERSION }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-template.yml b/.github/workflows/release-template.yml new file mode 100644 index 0000000..4bbe7d1 --- /dev/null +++ b/.github/workflows/release-template.yml @@ -0,0 +1,147 @@ +# .github/workflows/release-template.yml +# Automated release workflow for the cookiecutter-robust-python template +# Uses Calendar Versioning (CalVer): YYYY.MM.MICRO + +name: Release Template + +on: + push: + branches: + - main + + workflow_dispatch: + inputs: + micro_version: + description: 'Override micro version (leave empty for auto-increment)' + required: false + type: string + +jobs: + bump_and_build: + name: Bump Version & Build + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.VERSION }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: ".github/workflows/.python-version" + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Bump version and generate changelog + run: | + if [ -n "${{ inputs.micro_version }}" ]; then + uvx nox -s bump-version -- ${{ inputs.micro_version }} + else + uvx nox -s bump-version + fi + + - name: Get version + id: version + run: | + VERSION=$(uvx --from commitizen cz version -p) + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + + - name: Push version bump commit + run: git push origin HEAD + + - name: Build packages + run: uvx nox -s build-python + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist-${{ steps.version.outputs.VERSION }} + path: dist/ + retention-days: 7 + + publish_testpypi: + name: Publish to TestPyPI + runs-on: ubuntu-latest + needs: bump_and_build + permissions: + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist-${{ needs.bump_and_build.outputs.version }} + path: dist/ + + - name: Publish to TestPyPI + run: uvx nox -s publish-python -- --test-pypi + + publish_pypi: + name: Tag & Publish to PyPI + runs-on: ubuntu-latest + needs: [bump_and_build, publish_testpypi] + permissions: + id-token: write + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: main + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: ".github/workflows/.python-version" + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Pull latest (includes version bump) + run: git pull origin main + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist-${{ needs.bump_and_build.outputs.version }} + path: dist/ + + - name: Create and push tag + run: uvx nox -s tag-version -- push + + - name: Publish to PyPI + run: uvx nox -s publish-python + + - name: Extract release notes + run: uvx nox -s get-release-notes -- release_notes.md + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.bump_and_build.outputs.version }} + name: v${{ needs.bump_and_build.outputs.version }} + body_path: release_notes.md + files: dist/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/noxfile.py b/noxfile.py index b926add..2aa3ced 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,12 +1,17 @@ """Noxfile for the cookiecutter-robust-python template.""" # /// script -# dependencies = ["nox>=2025.5.1", "platformdirs>=4.3.8", "python-dotenv>=1.0.0"] +# dependencies = ["nox>=2025.5.1", "platformdirs>=4.3.8", "python-dotenv>=1.0.0", "tomli>=2.0.0;python_version<'3.11'"] # /// import os import shutil from dataclasses import asdict + +try: + import tomllib +except ModuleNotFoundError: + import tomli as tomllib from dataclasses import dataclass from pathlib import Path from typing import Any @@ -225,41 +230,82 @@ def merge_demo_feature(session: Session, demo: RepoMetadata) -> None: session.run("uv", "run", MERGE_DEMO_FEATURE_SCRIPT, *args) -@nox.session(python=False, name="release-template") -def release_template(session: Session): - """Run the release process for the TEMPLATE using Commitizen. +BUMP_VERSION_SCRIPT: Path = SCRIPTS_FOLDER / "bump-version.py" + + +@nox.session(python=False, name="bump-version") +def bump_version(session: Session) -> None: + """Bump version using CalVer (YYYY.MM.MICRO). - Requires uvx in PATH (from uv install). Requires Git. - Assumes Conventional Commits practice is followed for TEMPLATE repository. - Optionally accepts increment level (major, minor, patch) after '--'. + Usage: + nox -s bump-version # Auto-increment micro for current month + nox -s bump-version -- 5 # Force micro version to 5 """ - session.log("Running release process for the TEMPLATE using Commitizen...") - try: - session.run("git", "version", success_codes=[0], external=True, silent=True) - except CommandFailed: - session.log("Git command not found. Commitizen requires Git.") - session.skip("Git not available.") - - session.log("Checking Commitizen availability via uvx.") - session.run("cz", "--version", successcodes=[0]) - - increment = session.posargs[0] if session.posargs else None - session.log( - "Bumping template version and tagging release (increment: %s).", - increment if increment else "default", - ) + session.run("python", BUMP_VERSION_SCRIPT, *session.posargs, external=True) + + +@nox.session(python=False, name="build-python") +def build_python(session: Session) -> None: + """Build sdist and wheel packages for the template.""" + session.log("Building sdist and wheel packages...") + dist_dir = REPO_ROOT / "dist" + if dist_dir.exists(): + shutil.rmtree(dist_dir) + session.run("uv", "build", external=True) + session.log(f"Packages built in {dist_dir}") + + +@nox.session(python=False, name="publish-python") +def publish_python(session: Session) -> None: + """Publish packages to PyPI. + + Usage: + nox -s publish-python # Publish to PyPI + nox -s publish-python -- --test-pypi # Publish to TestPyPI + """ + session.log("Checking built packages with Twine.") + session.run("uvx", "twine", "check", "dist/*", external=True) - cz_bump_args = ["uvx", "cz", "bump", "--changelog"] + if "--test-pypi" in session.posargs: + session.log("Publishing packages to TestPyPI.") + session.run("uv", "publish", "--publish-url", "https://test.pypi.org/legacy/", external=True) + else: + session.log("Publishing packages to PyPI.") + session.run("uv", "publish", external=True) - if increment: - cz_bump_args.append(f"--increment={increment}") - session.log("Running cz bump with args: %s", cz_bump_args) - # success_codes=[0, 1] -> Allows code 1 which means 'nothing to bump' if no conventional commits since last release - session.run(*cz_bump_args, success_codes=[0, 1], external=True) +@nox.session(python=False, name="tag-version") +def tag_version(session: Session) -> None: + """Create and push a git tag for the current version. - session.log("Template version bumped and tag created locally via Commitizen/uvx.") - session.log("IMPORTANT: Push commits and tags to remote (`git push --follow-tags`) to trigger CD for the TEMPLATE.") + Usage: + nox -s tag-version # Create tag locally + nox -s tag-version -- push # Create and push tag + """ + with open(REPO_ROOT / "pyproject.toml", "rb") as f: + version = tomllib.load(f)["project"]["version"] + + tag_name = f"v{version}" + session.log(f"Creating tag: {tag_name}") + session.run("git", "tag", "-a", tag_name, "-m", f"Release {version}", external=True) + + if "push" in session.posargs: + session.log(f"Pushing tag {tag_name} to origin...") + session.run("git", "push", "origin", tag_name, external=True) + + +GET_RELEASE_NOTES_SCRIPT: Path = SCRIPTS_FOLDER / "get-release-notes.py" + + +@nox.session(python=False, name="get-release-notes") +def get_release_notes(session: Session) -> None: + """Extract release notes for the current version. + + Usage: + nox -s get-release-notes # Write to release_notes.md + nox -s get-release-notes -- /path/to/file.md # Write to custom path + """ + session.run("python", GET_RELEASE_NOTES_SCRIPT, *session.posargs, external=True) @nox.session(python=False, name="remove-demo-release") diff --git a/pyproject.toml b/pyproject.toml index 1ce0604..5d42991 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cookiecutter-robust-python" -version = "0.1.0" +version = "2025.11.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.10,<4.0" diff --git a/scripts/bump-version.py b/scripts/bump-version.py new file mode 100644 index 0000000..e17573e --- /dev/null +++ b/scripts/bump-version.py @@ -0,0 +1,49 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "python-dotenv", +# "typer", +# "tomli>=2.0.0;python_version<'3.11'", +# ] +# /// +"""Script responsible for bumping the version of cookiecutter-robust-python using CalVer.""" + +import sys +from typing import Annotated +from typing import Optional + +import typer + +from util import bump_version +from util import calculate_calver +from util import get_current_version + + +cli: typer.Typer = typer.Typer() + + +@cli.callback(invoke_without_command=True) +def main( + micro: Annotated[Optional[int], typer.Argument(help="Override micro version (default: auto-increment)")] = None, +) -> None: + """Bump version using CalVer (YYYY.MM.MICRO). + + CalVer format: + - YYYY: Four-digit year + - MM: Month (1-12, no leading zero) + - MICRO: Incremental patch number, resets to 0 each month + """ + try: + current_version: str = get_current_version() + new_version: str = calculate_calver(current_version, micro) + + typer.secho(f"Bumping version: {current_version} -> {new_version}", fg="blue") + bump_version(new_version) + typer.secho(f"Version bumped to {new_version}", fg="green") + except Exception as error: + typer.secho(f"error: {error}", fg="red") + sys.exit(1) + + +if __name__ == "__main__": + cli() diff --git a/scripts/get-release-notes.py b/scripts/get-release-notes.py new file mode 100644 index 0000000..3d982c2 --- /dev/null +++ b/scripts/get-release-notes.py @@ -0,0 +1,48 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "python-dotenv", +# "typer", +# ] +# /// +"""Script responsible for extracting release notes for the cookiecutter-robust-python template.""" + +import sys +from pathlib import Path +from typing import Annotated +from typing import Optional + +import typer + +from util import get_latest_release_notes + + +cli: typer.Typer = typer.Typer() + +DEFAULT_RELEASE_NOTES_PATH: Path = Path("release_notes.md") + + +@cli.callback(invoke_without_command=True) +def main( + path: Annotated[ + Optional[Path], + typer.Argument(help=f"Path to write release notes (default: {DEFAULT_RELEASE_NOTES_PATH})") + ] = None, +) -> None: + """Extract release notes for the current version. + + Uses commitizen to generate changelog entries for unreleased changes. + Must be run before tagging the release. + """ + try: + output_path: Path = path if path else DEFAULT_RELEASE_NOTES_PATH + release_notes: str = get_latest_release_notes() + output_path.write_text(release_notes) + typer.secho(f"Release notes written to {output_path}", fg="green") + except Exception as error: + typer.secho(f"error: {error}", fg="red") + sys.exit(1) + + +if __name__ == "__main__": + cli() diff --git a/scripts/util.py b/scripts/util.py index 2844afb..696da96 100644 --- a/scripts/util.py +++ b/scripts/util.py @@ -4,6 +4,7 @@ # "cookiecutter", # "cruft", # "python-dotenv", +# "tomli>=2.0.0;python_version<'3.11'", # "typer", # ] # /// @@ -248,3 +249,111 @@ def _remove_existing_demo(demo_path: Path) -> None: def get_demo_name(add_rust_extension: bool) -> str: name_modifier: str = "maturin" if add_rust_extension else "python" return f"robust-{name_modifier}-demo" + + +def get_package_version() -> str: + """Gets the current package version using commitizen.""" + result = run_command("uvx", "--from", "commitizen", "cz", "version", "-p") + return result.stdout.strip() + + +def get_current_version() -> str: + """Read current version from pyproject.toml.""" + try: + import tomllib + except ModuleNotFoundError: + import tomli as tomllib + + pyproject_path: Path = REPO_FOLDER / "pyproject.toml" + with pyproject_path.open("rb") as f: + data: dict[str, Any] = tomllib.load(f) + return data["project"]["version"] + + +def calculate_calver(current_version: str, micro_override: Optional[int] = None) -> str: + """Calculate the next CalVer version. + + CalVer format: YYYY.MM.MICRO + - YYYY: Four-digit year + - MM: Month (1-12, no leading zero) + - MICRO: Incremental patch number, resets to 0 each month + + Args: + current_version: The current version string + micro_override: Optional manual micro version override + + Returns: + The new CalVer version string (YYYY.MM.MICRO) + """ + from datetime import date + + today = date.today() + year, month = today.year, today.month + + if micro_override is not None: + micro = micro_override + else: + # Auto-calculate micro + try: + parts: list[str] = current_version.split(".") + curr_year, curr_month, curr_micro = int(parts[0]), int(parts[1]), int(parts[2]) + if curr_year == year and curr_month == month: + micro = curr_micro + 1 # Same month, increment + else: + micro = 0 # New month, reset + except (ValueError, IndexError): + micro = 0 # Invalid version format, start fresh + + return f"{year}.{month}.{micro}" + + +def bump_version(new_version: str) -> None: + """Bump version using commitizen. + + Args: + new_version: The version to bump to + """ + cmd: list[str] = ["uvx", "--from", "commitizen", "cz", "bump", "--changelog", "--yes", "--no-tag", new_version] + # Exit code 1 means 'nothing to bump' - treat as success + result: subprocess.CompletedProcess = subprocess.run(cmd, cwd=REPO_FOLDER) + if result.returncode not in (0, 1): + raise RuntimeError(f"Version bump failed with exit code {result.returncode}") + + +def get_latest_tag() -> Optional[str]: + """Gets the latest git tag, or None if no tags exist.""" + result = run_command("git", "describe", "--tags", "--abbrev=0", ignore_error=True) + if result is None: + return None + tag = result.stdout.strip() + return tag if tag else None + + +def get_latest_release_notes() -> str: + """Gets the release notes for the current version. + + Assumes the tag hasn't been applied yet. + """ + latest_tag: Optional[str] = get_latest_tag() + latest_version: str = get_package_version() + + # Build the revision range for changelog + if latest_tag is None: + rev_range = "" + else: + # Strip 'v' prefix if present for comparison + tag_version = latest_tag.lstrip("v") + if tag_version == latest_version: + raise ValueError( + "The latest tag and version are the same. " + "Please ensure the release notes are taken before tagging." + ) + rev_range = f"{latest_tag}.." + + result = run_command( + "uvx", "--from", "commitizen", "cz", "changelog", + rev_range, + "--dry-run", + "--unreleased-version", latest_version + ) + return result.stdout From 34fc165e01c7c3daa0f04780f6ad3071545adc9d Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sat, 29 Nov 2025 22:36:55 -0500 Subject: [PATCH 09/43] docs: update CONTRIBUTING.md info --- CONTRIBUTING.md | 121 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 110 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5dca0c8..6746e4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,20 +14,121 @@ There are several ways to contribute: ## Setting Up Your Development Environment -Refer to the **[Getting Started: Contributing to the Template](https://robust-python.github.io/cookiecutter-robust-python/getting-started-template-contributing.html)** section in the template documentation for instructions on cloning the repository, installing template development dependencies (using uv), setting up the template's pre-commit hooks, and running template checks/tests. +1. **Clone** the repository: + ```bash + git clone https://github.com/robust-python/cookiecutter-robust-python.git + cd cookiecutter-robust-python + ``` + +2. **Install dependencies** using uv: + ```bash + uv sync --all-groups + ``` + +3. **Install pre-commit hooks**: + ```bash + uvx nox -s pre-commit -- install + ``` + +4. **Generate a demo project** to test changes: + ```bash + nox -s generate-demo + ``` + +Refer to the **[Getting Started: Contributing to the Template](https://robust-python.github.io/cookiecutter-robust-python/getting-started-template-contributing.html)** section in the template documentation for more detailed instructions. + +## Development Commands + +### Code Quality +```bash +# Lint the template source code +nox -s lint + +# Lint from generated demo project +nox -s lint-from-demo + +# Run template tests +nox -s test + +# Build template documentation +nox -s docs +``` + +### Demo Projects +```bash +# Generate a demo project for testing +nox -s generate-demo + +# Generate demo with Rust extension +nox -s generate-demo -- --add-rust-extension + +# Update existing demo projects +nox -s update-demo + +# Clear demo cache +nox -s clear-cache +``` ## Contribution Workflow 1. **Fork** the repository and **clone** your fork. -2. Create a **new branch** for your contribution based on the main branch. Use a descriptive name (e.g., `fix/ci-workflow-on-windows`, `feat/update-uv-version`). -3. Set up your development environment following the [Getting Started](https://robust-python.github.io/cookiecutter-robust-python/getting-started-template-contributing.html) guide (clone, `uv sync`, `uvx nox -s pre-commit -- install`). +2. Create a **new branch** for your contribution based on the `develop` branch. Use a descriptive name (e.g., `fix/ci-workflow-on-windows`, `feat/update-uv-version`). +3. Set up your development environment as described above. 4. Make your **code or documentation changes**. -5. Ensure your changes adhere to the template's **code quality standards** (configured in the template's `.pre-commit-config.yaml`, `.ruff.toml`, etc.). The pre-commit hooks will help with this. Run `uvx nox -s lint`, `uvx nox -s check` in the template repository for more comprehensive checks. -6. Ensure your changes **do not break existing functionality**. Run the template's test suite: `uvx nox -s test`. Ideally, add tests for new functionality or bug fixes. -7. Ensure the **template documentation builds correctly** with your changes: `uvx nox -s docs`. -8. Write clear, concise **commit messages** following the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification where possible, especially for significant changes (fixes, features, chore updates, etc.). -9. **Push** your branch to your fork. -10. **Open a Pull Request** from your branch to the main branch of the main template repository. Provide a clear description of your changes. Link to any relevant issues. +5. Ensure your changes adhere to the template's **code quality standards**. Run: + ```bash + nox -s lint + nox -s test + ``` +6. Ensure the **template documentation builds correctly**: + ```bash + nox -s docs + ``` +7. Write clear, concise **commit messages** following the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. This is **required** as we use Commitizen to generate changelogs automatically. +8. **Push** your branch to your fork. +9. **Open a Pull Request** from your branch to the `develop` branch of the main repository. Provide a clear description of your changes. Link to any relevant issues. + +## Commit Message Guidelines + +We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages. This enables automatic changelog generation via Commitizen. + +### Format +``` +(): + +[optional body] + +[optional footer(s)] +``` + +### Types +- `feat`: A new feature +- `fix`: A bug fix +- `docs`: Documentation only changes +- `style`: Changes that do not affect the meaning of the code +- `refactor`: A code change that neither fixes a bug nor adds a feature +- `perf`: A code change that improves performance +- `test`: Adding missing tests or correcting existing tests +- `build`: Changes that affect the build system or external dependencies +- `ci`: Changes to CI configuration files and scripts +- `chore`: Other changes that don't modify src or test files + +### Examples +``` +feat(template): add support for Python 3.13 +fix(ci): correct workflow trigger for demo sync +docs(readme): update installation instructions +chore(deps): bump ruff to 0.12.0 +``` + +## Versioning + +This template uses **Calendar Versioning (CalVer)** with the format `YYYY.MM.MICRO`: +- `YYYY`: Four-digit year +- `MM`: Month (1-12, no leading zero) +- `MICRO`: Incremental patch number, resets to 0 each new month + +Releases are handled automatically via CI when changes are merged to `main`. Contributors do not need to bump versions manually. ## Updating Tool Evaluations @@ -36,5 +137,3 @@ If your contribution involves updating a major tool version or suggesting a diff ## Communication For questions or discussion about contributions, open an issue or a discussion on the [GitHub repository](https://github.com/robust-python/cookiecutter-robust-python). - ---- From 4345fa5fe055f2048f7d5f6f12a4f986a4ea8c60 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sun, 30 Nov 2025 00:53:45 -0500 Subject: [PATCH 10/43] docs: fix pre-commit install method in CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6746e4c..106a087 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ There are several ways to contribute: 3. **Install pre-commit hooks**: ```bash - uvx nox -s pre-commit -- install + uvx pre-commit install ``` 4. **Generate a demo project** to test changes: From 7d4cc40dd7eabd8ceb44436deed964aa1867f41e Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sun, 30 Nov 2025 01:25:40 -0500 Subject: [PATCH 11/43] refactor: move get_current_version into bump-version script to avoid unneeded tomli install in other scripts --- scripts/bump-version.py | 19 ++++++++++++++++++- scripts/util.py | 14 -------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/scripts/bump-version.py b/scripts/bump-version.py index e17573e..1d17192 100644 --- a/scripts/bump-version.py +++ b/scripts/bump-version.py @@ -9,14 +9,22 @@ """Script responsible for bumping the version of cookiecutter-robust-python using CalVer.""" import sys +from pathlib import Path from typing import Annotated +from typing import Any from typing import Optional import typer from util import bump_version from util import calculate_calver -from util import get_current_version +from util import REPO_FOLDER + + +try: + import tomllib +except ModuleNotFoundError: + import tomli as tomllib cli: typer.Typer = typer.Typer() @@ -45,5 +53,14 @@ def main( sys.exit(1) +def get_current_version() -> str: + """Read current version from pyproject.toml.""" + + pyproject_path: Path = REPO_FOLDER / "pyproject.toml" + with pyproject_path.open("rb") as f: + data: dict[str, Any] = tomllib.load(f) + return data["project"]["version"] + + if __name__ == "__main__": cli() diff --git a/scripts/util.py b/scripts/util.py index 696da96..ee8226c 100644 --- a/scripts/util.py +++ b/scripts/util.py @@ -4,7 +4,6 @@ # "cookiecutter", # "cruft", # "python-dotenv", -# "tomli>=2.0.0;python_version<'3.11'", # "typer", # ] # /// @@ -257,19 +256,6 @@ def get_package_version() -> str: return result.stdout.strip() -def get_current_version() -> str: - """Read current version from pyproject.toml.""" - try: - import tomllib - except ModuleNotFoundError: - import tomli as tomllib - - pyproject_path: Path = REPO_FOLDER / "pyproject.toml" - with pyproject_path.open("rb") as f: - data: dict[str, Any] = tomllib.load(f) - return data["project"]["version"] - - def calculate_calver(current_version: str, micro_override: Optional[int] = None) -> str: """Calculate the next CalVer version. From e2940a773ea4f5695581bf97eda9f7dff133fe16 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sun, 30 Nov 2025 01:42:11 -0500 Subject: [PATCH 12/43] refactor: move tag-version into its own script for the time being Will most likely be recondensed at a later point, but keeping the whole script pattern consistent for easier refactoring later --- noxfile.py | 40 ++++++++------------------- scripts/tag-version.py | 62 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 28 deletions(-) create mode 100644 scripts/tag-version.py diff --git a/noxfile.py b/noxfile.py index 2aa3ced..a7af8c2 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,17 +1,12 @@ """Noxfile for the cookiecutter-robust-python template.""" # /// script -# dependencies = ["nox>=2025.5.1", "platformdirs>=4.3.8", "python-dotenv>=1.0.0", "tomli>=2.0.0;python_version<'3.11'"] +# dependencies = ["nox>=2025.5.1", "platformdirs>=4.3.8", "python-dotenv>=1.0.0"] # /// import os import shutil from dataclasses import asdict - -try: - import tomllib -except ModuleNotFoundError: - import tomli as tomllib from dataclasses import dataclass from pathlib import Path from typing import Any @@ -19,7 +14,6 @@ import nox import platformdirs from dotenv import load_dotenv -from nox.command import CommandFailed from nox.sessions import Session @@ -31,7 +25,6 @@ SCRIPTS_FOLDER: Path = REPO_ROOT / "scripts" TEMPLATE_FOLDER: Path = REPO_ROOT / "{{cookiecutter.project_name}}" - # Load environment variables from .env and .env.local (if present) LOCAL_ENV_FILE: Path = REPO_ROOT / ".env.local" DEFAULT_ENV_FILE: Path = REPO_ROOT / ".env" @@ -52,9 +45,11 @@ ).resolve() DEFAULT_DEMOS_CACHE_FOLDER = COOKIECUTTER_ROBUST_PYTHON_CACHE_FOLDER / "project_demos" -DEMOS_CACHE_FOLDER: Path = Path(os.getenv( - "COOKIECUTTER_ROBUST_PYTHON__DEMOS_CACHE_FOLDER", default=DEFAULT_DEMOS_CACHE_FOLDER -)).resolve() +DEMOS_CACHE_FOLDER: Path = Path( + os.getenv( + "COOKIECUTTER_ROBUST_PYTHON__DEMOS_CACHE_FOLDER", default=DEFAULT_DEMOS_CACHE_FOLDER + ) +).resolve() DEFAULT_DEMO_NAME: str = "robust-python-demo" DEMO_ROOT_FOLDER: Path = DEMOS_CACHE_FOLDER / DEFAULT_DEMO_NAME @@ -76,6 +71,10 @@ MERGE_DEMO_FEATURE_SCRIPT: Path = SCRIPTS_FOLDER / "merge-demo-feature.py" MERGE_DEMO_FEATURE_OPTIONS: tuple[str, ...] = GENERATE_DEMO_OPTIONS +BUMP_VERSION_SCRIPT: Path = SCRIPTS_FOLDER / "bump-version.py" +GET_RELEASE_NOTES_SCRIPT: Path = SCRIPTS_FOLDER / "get-release-notes.py" +TAG_VERSION_SCRIPT: Path = SCRIPTS_FOLDER / "tag-version.py" + @dataclass class RepoMetadata: @@ -230,9 +229,6 @@ def merge_demo_feature(session: Session, demo: RepoMetadata) -> None: session.run("uv", "run", MERGE_DEMO_FEATURE_SCRIPT, *args) -BUMP_VERSION_SCRIPT: Path = SCRIPTS_FOLDER / "bump-version.py" - - @nox.session(python=False, name="bump-version") def bump_version(session: Session) -> None: """Bump version using CalVer (YYYY.MM.MICRO). @@ -282,19 +278,8 @@ def tag_version(session: Session) -> None: nox -s tag-version # Create tag locally nox -s tag-version -- push # Create and push tag """ - with open(REPO_ROOT / "pyproject.toml", "rb") as f: - version = tomllib.load(f)["project"]["version"] - - tag_name = f"v{version}" - session.log(f"Creating tag: {tag_name}") - session.run("git", "tag", "-a", tag_name, "-m", f"Release {version}", external=True) - - if "push" in session.posargs: - session.log(f"Pushing tag {tag_name} to origin...") - session.run("git", "push", "origin", tag_name, external=True) - - -GET_RELEASE_NOTES_SCRIPT: Path = SCRIPTS_FOLDER / "get-release-notes.py" + args: list[str] = ["--push"] if "push" in session.posargs else [] + session.run("python", TAG_VERSION_SCRIPT, *args, external=True) @nox.session(python=False, name="get-release-notes") @@ -313,4 +298,3 @@ def remove_demo_release(session: Session) -> None: """Deletes the latest demo release.""" session.run("git", "branch", "-d", f"release/{session.posargs[0]}", external=True) session.run("git", "push", "--progress", "--porcelain", "origin", f"release/{session.posargs[0]}", external=True) - diff --git a/scripts/tag-version.py b/scripts/tag-version.py new file mode 100644 index 0000000..0023291 --- /dev/null +++ b/scripts/tag-version.py @@ -0,0 +1,62 @@ +"""Script responsible for creating and pushing git tags for cookiecutter-robust-python releases.""" +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "python-dotenv", +# "typer", +# "tomli>=2.0.0;python_version<'3.11'", +# ] +# /// + +from pathlib import Path +from typing import Annotated +from typing import Any + +import typer +from cookiecutter.utils import work_in + +from scripts.util import git +from util import REPO_FOLDER + + +try: + import tomllib +except ModuleNotFoundError: + import tomli as tomllib + + +cli: typer.Typer = typer.Typer() + + +@cli.callback(invoke_without_command=True) +def main( + push: Annotated[bool, typer.Option("--push", help="Push the tag to origin after creating it")] = False +) -> None: + """Create a git tag for the current version. + + Creates an annotated tag in the format 'vYYYY.MM.MICRO' based on the + version in pyproject.toml. Optionally pushes the tag to origin. + """ + version: str = get_current_version() + tag_name: str = f"v{version}" + with work_in(REPO_FOLDER): + typer.secho(f"Creating tag: {tag_name}", fg="blue") + git("tag", "-a", tag_name, "-m", f"Release {version}") + typer.secho(f"Tag {tag_name} created successfully", fg="green") + + if push: + typer.secho(f"Pushing tag {tag_name} to origin...", fg="blue") + git("push", "origin", tag_name) + typer.secho(f"Tag {tag_name} pushed to origin", fg="green") + + +def get_current_version() -> str: + """Read current version from pyproject.toml.""" + pyproject_path: Path = REPO_FOLDER / "pyproject.toml" + with pyproject_path.open("rb") as f: + data: dict[str, Any] = tomllib.load(f) + return data["project"]["version"] + + +if __name__ == "__main__": + cli() From 782ea9b2fd0416fc002bda3032bceede20169035 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sun, 30 Nov 2025 01:46:36 -0500 Subject: [PATCH 13/43] chore: update uv.lock --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 7e409a7..32deb56 100644 --- a/uv.lock +++ b/uv.lock @@ -356,7 +356,7 @@ wheels = [ [[package]] name = "cookiecutter-robust-python" -version = "0.1.0" +version = "2025.11.0" source = { virtual = "." } dependencies = [ { name = "cookiecutter" }, From 3483e9e38f95d0437dc803cfd09db2050ab10194 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sun, 30 Nov 2025 01:50:28 -0500 Subject: [PATCH 14/43] fix: replace uv run call in update-demo nox session with install_and_run_script --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index a7af8c2..989fdbe 100644 --- a/noxfile.py +++ b/noxfile.py @@ -207,7 +207,7 @@ def update_demo(session: Session, demo: RepoMetadata) -> None: args.extend(session.posargs) demo_env: dict[str, Any] = {f"ROBUST_DEMO__{key.upper()}": value for key, value in asdict(demo).items()} - session.run("uv", "run", UPDATE_DEMO_SCRIPT, *args, env=demo_env) + session.install_and_run_script(UPDATE_DEMO_SCRIPT, *args, env=demo_env) @nox.parametrize( From 58f3520c775910f9f107bd03463b10b063400a28 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sun, 30 Nov 2025 02:22:43 -0500 Subject: [PATCH 15/43] fix: update names throughout nox session and file along with fix update-demo not creating branches and some env var issues --- noxfile.py | 7 +++---- scripts/update-demo.py | 17 ++++++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/noxfile.py b/noxfile.py index 989fdbe..aa02e8e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -35,8 +35,8 @@ if DEFAULT_ENV_FILE.exists(): load_dotenv(DEFAULT_ENV_FILE) -APP_AUTHOR: str = os.getenv("COOKIECUTTER_ROBUST_PYTHON_APP_AUTHOR", "robust-python") -COOKIECUTTER_ROBUST_PYTHON_CACHE_FOLDER: Path = Path( +APP_AUTHOR: str = os.getenv("COOKIECUTTER_ROBUST_PYTHON__APP_AUTHOR", "robust-python") +COOKIECUTTER_ROBUST_PYTHON__CACHE_FOLDER: Path = Path( platformdirs.user_cache_path( appname="cookiecutter-robust-python", appauthor=APP_AUTHOR, @@ -44,14 +44,13 @@ ) ).resolve() -DEFAULT_DEMOS_CACHE_FOLDER = COOKIECUTTER_ROBUST_PYTHON_CACHE_FOLDER / "project_demos" +DEFAULT_DEMOS_CACHE_FOLDER = COOKIECUTTER_ROBUST_PYTHON__CACHE_FOLDER / "project_demos" DEMOS_CACHE_FOLDER: Path = Path( os.getenv( "COOKIECUTTER_ROBUST_PYTHON__DEMOS_CACHE_FOLDER", default=DEFAULT_DEMOS_CACHE_FOLDER ) ).resolve() DEFAULT_DEMO_NAME: str = "robust-python-demo" -DEMO_ROOT_FOLDER: Path = DEMOS_CACHE_FOLDER / DEFAULT_DEMO_NAME GENERATE_DEMO_SCRIPT: Path = SCRIPTS_FOLDER / "generate-demo.py" GENERATE_DEMO_OPTIONS: tuple[str, ...] = ( diff --git a/scripts/update-demo.py b/scripts/update-demo.py index a300991..e8da7f0 100644 --- a/scripts/update-demo.py +++ b/scripts/update-demo.py @@ -51,14 +51,14 @@ def update_demo( if branch_override is not None: typer.secho(f"Overriding current branch name for demo reference. Using '{branch_override}' instead.") - current_branch: str = branch_override + desired_branch_name: str = branch_override else: - current_branch: str = get_current_branch() + desired_branch_name: str = get_current_branch() template_commit: str = get_current_commit() - _validate_template_main_not_checked_out(branch=current_branch) + _validate_template_main_not_checked_out(branch=desired_branch_name) require_clean_and_up_to_date_demo_repo(demo_path=demo_path) - _checkout_demo_develop_or_existing_branch(demo_path=demo_path, branch=current_branch) + _checkout_demo_develop_or_existing_branch(demo_path=demo_path, branch=desired_branch_name) last_update_commit: str = get_last_cruft_update_commit(demo_path=demo_path) if not is_ancestor(last_update_commit, template_commit): @@ -69,6 +69,9 @@ def update_demo( typer.secho(f"Updating demo project at {demo_path=}.", fg="yellow") with work_in(demo_path): + if get_current_branch() != desired_branch_name: + git("checkout", "-b", desired_branch_name, DEMO.develop_branch) + uv("python", "pin", min_python_version) uv("python", "install", min_python_version) cruft.update( @@ -84,9 +87,9 @@ def update_demo( uv("lock") git("add", ".") git("commit", "-m", f"chore: {last_update_commit} -> {template_commit}", "--no-verify") - git("push", "-u", "origin", current_branch) - if current_branch != "develop": - _create_demo_pr(demo_path=demo_path, branch=current_branch, commit_start=last_update_commit) + git("push", "-u", "origin", desired_branch_name) + if desired_branch_name != "develop": + _create_demo_pr(demo_path=demo_path, branch=desired_branch_name, commit_start=last_update_commit) def _checkout_demo_develop_or_existing_branch(demo_path: Path, branch: str) -> None: From 8017439d221e07c623d84e34a454bdb14b32562e Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sun, 30 Nov 2025 02:41:16 -0500 Subject: [PATCH 16/43] chore: manually set environ variable in pytest demos_folder fixture so that it remains isolated from CI interactions --- tests/conftest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 66b34da..e9e2fdb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ """Fixtures used in all tests for cookiecutter-robust-python.""" - +import os import subprocess from pathlib import Path from typing import Any @@ -21,7 +21,9 @@ @pytest.fixture(scope="session") def demos_folder(tmp_path_factory: TempPathFactory) -> Path: """Temp Folder used for storing demos while testing.""" - return tmp_path_factory.mktemp("demos") + path: Path = tmp_path_factory.mktemp("demos") + os.environ["COOKIECUTTER_ROBUST_PYTHON__DEMOS_CACHE_FOLDER"] = str(path) + return path @pytest.fixture(scope="session") From bf45c15ec8123a0d65575859ceedac3af5f756a6 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sun, 30 Nov 2025 04:23:25 -0500 Subject: [PATCH 17/43] feat: add initial implementation of setup-release nox session and corresponding script prior to review --- noxfile.py | 15 +++++ scripts/setup-release.py | 128 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 scripts/setup-release.py diff --git a/noxfile.py b/noxfile.py index aa02e8e..9851cc8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -72,6 +72,7 @@ BUMP_VERSION_SCRIPT: Path = SCRIPTS_FOLDER / "bump-version.py" GET_RELEASE_NOTES_SCRIPT: Path = SCRIPTS_FOLDER / "get-release-notes.py" +SETUP_RELEASE_SCRIPT: Path = SCRIPTS_FOLDER / "setup-release.py" TAG_VERSION_SCRIPT: Path = SCRIPTS_FOLDER / "tag-version.py" @@ -228,6 +229,20 @@ def merge_demo_feature(session: Session, demo: RepoMetadata) -> None: session.run("uv", "run", MERGE_DEMO_FEATURE_SCRIPT, *args) +@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="setup-release") +def setup_release(session: Session) -> None: + """Prepare a release by creating a release branch and bumping the version. + + Creates a release branch from develop, bumps the version using CalVer, + and creates the initial bump commit. Does not push any changes. + + Usage: + nox -s setup-release # Auto-increment micro for current month + nox -s setup-release -- 5 # Force micro version to 5 + """ + session.install_and_run_script(SETUP_RELEASE_SCRIPT, *session.posargs) + + @nox.session(python=False, name="bump-version") def bump_version(session: Session) -> None: """Bump version using CalVer (YYYY.MM.MICRO). diff --git a/scripts/setup-release.py b/scripts/setup-release.py new file mode 100644 index 0000000..016bac0 --- /dev/null +++ b/scripts/setup-release.py @@ -0,0 +1,128 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "cookiecutter", +# "python-dotenv", +# "typer", +# "tomli>=2.0.0;python_version<'3.11'", +# ] +# /// +"""Script responsible for preparing a release of the cookiecutter-robust-python template.""" + +import sys +from pathlib import Path +from typing import Annotated +from typing import Any +from typing import Optional + +import typer +from cookiecutter.utils import work_in + +from util import bump_version +from util import calculate_calver +from util import git +from util import REPO_FOLDER +from util import TEMPLATE + + +try: + import tomllib +except ModuleNotFoundError: + import tomli as tomllib + + +cli: typer.Typer = typer.Typer() + + +@cli.callback(invoke_without_command=True) +def main( + micro: Annotated[ + Optional[int], + typer.Argument(help="Override micro version (default: auto-increment)") + ] = None, +) -> None: + """Prepare a release by creating a release branch and bumping the version. + + Creates a release branch from develop, bumps the version using CalVer, + and creates the initial bump commit. Does not push any changes. + + CalVer format: YYYY.MM.MICRO + """ + try: + current_version: str = get_current_version() + new_version: str = calculate_calver(current_version, micro) + + typer.secho(f"Setting up release: {current_version} -> {new_version}", fg="blue") + + setup_release(current_version=current_version, new_version=new_version, micro=micro) + + typer.secho(f"Release branch created: release/{new_version}", fg="green") + typer.secho("Next steps:", fg="blue") + typer.secho(f" 1. Review changes and push: git push -u origin release/{new_version}", fg="white") + typer.secho(" 2. Create a pull request to main", fg="white") + except Exception as error: + typer.secho(f"error: {error}", fg="red") + sys.exit(1) + + +def get_current_version() -> str: + """Read current version from pyproject.toml.""" + pyproject_path: Path = REPO_FOLDER / "pyproject.toml" + with pyproject_path.open("rb") as f: + data: dict[str, Any] = tomllib.load(f) + return data["project"]["version"] + + +def setup_release(current_version: str, new_version: str, micro: Optional[int] = None) -> None: + """Prepares a release of the cookiecutter-robust-python template. + + Creates a release branch from develop, bumps the version, and creates a release commit. + Rolls back on error. + """ + with work_in(REPO_FOLDER): + try: + _setup_release(current_version=current_version, new_version=new_version, micro=micro) + except Exception as error: + _rollback_release(version=new_version) + raise error + + +def _setup_release(current_version: str, new_version: str, micro: Optional[int] = None) -> None: + """Internal setup release logic.""" + develop_branch: str = TEMPLATE.develop_branch + release_branch: str = f"release/{new_version}" + + # Create release branch from develop + typer.secho(f"Creating branch {release_branch} from {develop_branch}...", fg="blue") + git("checkout", "-b", release_branch, develop_branch) + + # Bump version + typer.secho(f"Bumping version to {new_version}...", fg="blue") + bump_version(new_version) + + # Sync dependencies + typer.secho("Syncing dependencies...", fg="blue") + git("add", ".") + + # Create bump commit + typer.secho("Creating bump commit...", fg="blue") + git("commit", "-m", f"bump: version {current_version} → {new_version}") + + +def _rollback_release(version: str) -> None: + """Rolls back to the pre-existing state on error.""" + develop_branch: str = TEMPLATE.develop_branch + release_branch: str = f"release/{version}" + + typer.secho(f"Rolling back release {version}...", fg="yellow") + + # Checkout develop and discard changes + git("checkout", develop_branch, ignore_error=True) + git("checkout", ".", ignore_error=True) + + # Delete the release branch if it exists + git("branch", "-D", release_branch, ignore_error=True) + + +if __name__ == "__main__": + cli() From 63ebafa5b54a433286394fb432bc55aca0999d23 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sun, 30 Nov 2025 04:33:35 -0500 Subject: [PATCH 18/43] feat: add check for when the template is already in sync with the demo --- scripts/update-demo.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/update-demo.py b/scripts/update-demo.py index e8da7f0..b73e7f2 100644 --- a/scripts/update-demo.py +++ b/scripts/update-demo.py @@ -61,6 +61,12 @@ def update_demo( _checkout_demo_develop_or_existing_branch(demo_path=demo_path, branch=desired_branch_name) last_update_commit: str = get_last_cruft_update_commit(demo_path=demo_path) + if template_commit == last_update_commit: + typer.secho( + f"{demo_name} is already up to date with {desired_branch_name} at {last_update_commit}", + fg=typer.colors.YELLOW + ) + if not is_ancestor(last_update_commit, template_commit): raise ValueError( f"The last update commit '{last_update_commit}' is not an ancestor of the current commit " From fd27f005672b7020463a61714c9a873032dcf43a Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sun, 30 Nov 2025 04:40:27 -0500 Subject: [PATCH 19/43] fix: remove github.workspace from uses path --- .github/workflows/sync-demos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-demos.yml b/.github/workflows/sync-demos.yml index 239b49c..10be892 100644 --- a/.github/workflows/sync-demos.yml +++ b/.github/workflows/sync-demos.yml @@ -22,6 +22,6 @@ jobs: path: "${{ github.workspace }}/cookiecutter-robust-python" - name: Update Demo - uses: "${{ github.workspace }}/cookiecutter-robust-python/.github/workflows/update-demo.yml" + uses: "./cookiecutter-robust-python/.github/workflows/update-demo.yml" with: demo_name: ${{ matrix.demo_name }} From c940656a34baa8cff1112bfb6c075cd05c89e89d Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sun, 30 Nov 2025 04:42:49 -0500 Subject: [PATCH 20/43] fix: change paths pointing toward update-demo reusable workflow --- .github/workflows/merge-demo-feature.yml | 2 +- .github/workflows/sync-demos.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/merge-demo-feature.yml b/.github/workflows/merge-demo-feature.yml index 6ab3f83..ab32a0c 100644 --- a/.github/workflows/merge-demo-feature.yml +++ b/.github/workflows/merge-demo-feature.yml @@ -27,7 +27,7 @@ jobs: path: "${{ github.workspace }}/cookiecutter-robust-python" - name: Sync Demo - uses: "${{ github.workspace }}/cookiecutter-robust-python/.github/workflows/update-demo.yml" + uses: "./.github/workflows/update-demo.yml" with: demo_name: ${{ matrix.demo_name }} diff --git a/.github/workflows/sync-demos.yml b/.github/workflows/sync-demos.yml index 10be892..54a79e6 100644 --- a/.github/workflows/sync-demos.yml +++ b/.github/workflows/sync-demos.yml @@ -22,6 +22,6 @@ jobs: path: "${{ github.workspace }}/cookiecutter-robust-python" - name: Update Demo - uses: "./cookiecutter-robust-python/.github/workflows/update-demo.yml" + uses: "./.github/workflows/update-demo.yml" with: demo_name: ${{ matrix.demo_name }} From 774807233d2ff97ff41cbba6101fc8c8b0aa053e Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sun, 30 Nov 2025 04:53:13 -0500 Subject: [PATCH 21/43] chore: lint a few scripts --- scripts/lint-from-demo.py | 2 -- scripts/setup-release.py | 1 - 2 files changed, 3 deletions(-) diff --git a/scripts/lint-from-demo.py b/scripts/lint-from-demo.py index c9458a9..2ae5a3f 100644 --- a/scripts/lint-from-demo.py +++ b/scripts/lint-from-demo.py @@ -9,7 +9,6 @@ # ] # /// -import os from pathlib import Path from typing import Annotated @@ -30,7 +29,6 @@ "uv.lock", ] - cli: typer.Typer = typer.Typer() diff --git a/scripts/setup-release.py b/scripts/setup-release.py index 016bac0..52ab9f3 100644 --- a/scripts/setup-release.py +++ b/scripts/setup-release.py @@ -30,7 +30,6 @@ except ModuleNotFoundError: import tomli as tomllib - cli: typer.Typer = typer.Typer() From e1297995cda14fe71360c5da6ab81fd16322bdb9 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Sun, 30 Nov 2025 04:59:38 -0500 Subject: [PATCH 22/43] fix: tweak gitlab ci to not error from too many uv cache keyfiles being defined --- {{cookiecutter.project_name}}/.gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/{{cookiecutter.project_name}}/.gitlab-ci.yml b/{{cookiecutter.project_name}}/.gitlab-ci.yml index 6edd465..e766f84 100644 --- a/{{cookiecutter.project_name}}/.gitlab-ci.yml +++ b/{{cookiecutter.project_name}}/.gitlab-ci.yml @@ -26,8 +26,6 @@ stages: files: - pyproject.toml - uv.lock - - requirements*.txt - - "**/requirements*.txt" paths: - $UV_CACHE_DIR - $PIP_CACHE_DIR From 2f4adf2bdfc051898caeaabe420fad15d1da7ee1 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 01:46:56 -0500 Subject: [PATCH 23/43] chore: alter initial checkout location to see if it fixes actions references --- .github/workflows/merge-demo-feature.yml | 2 +- .github/workflows/sync-demos.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/merge-demo-feature.yml b/.github/workflows/merge-demo-feature.yml index ab32a0c..8a8a173 100644 --- a/.github/workflows/merge-demo-feature.yml +++ b/.github/workflows/merge-demo-feature.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v4 with: repository: ${{ github.repository }} - path: "${{ github.workspace }}/cookiecutter-robust-python" + path: "${{ github.workspace }}" - name: Sync Demo uses: "./.github/workflows/update-demo.yml" diff --git a/.github/workflows/sync-demos.yml b/.github/workflows/sync-demos.yml index 54a79e6..7f93e43 100644 --- a/.github/workflows/sync-demos.yml +++ b/.github/workflows/sync-demos.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v4 with: repository: ${{ github.repository }} - path: "${{ github.workspace }}/cookiecutter-robust-python" + path: "${{ github.workspace }}" - name: Update Demo uses: "./.github/workflows/update-demo.yml" From e74104f7625712a19643c91259177dcdf1e0bf43 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 01:50:46 -0500 Subject: [PATCH 24/43] chore: remove path provided to see if it fixes reference issues --- .github/workflows/merge-demo-feature.yml | 1 - .github/workflows/sync-demos.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/merge-demo-feature.yml b/.github/workflows/merge-demo-feature.yml index 8a8a173..3595ca2 100644 --- a/.github/workflows/merge-demo-feature.yml +++ b/.github/workflows/merge-demo-feature.yml @@ -24,7 +24,6 @@ jobs: uses: actions/checkout@v4 with: repository: ${{ github.repository }} - path: "${{ github.workspace }}" - name: Sync Demo uses: "./.github/workflows/update-demo.yml" diff --git a/.github/workflows/sync-demos.yml b/.github/workflows/sync-demos.yml index 7f93e43..4477e82 100644 --- a/.github/workflows/sync-demos.yml +++ b/.github/workflows/sync-demos.yml @@ -19,7 +19,6 @@ jobs: uses: actions/checkout@v4 with: repository: ${{ github.repository }} - path: "${{ github.workspace }}" - name: Update Demo uses: "./.github/workflows/update-demo.yml" From 13bf965da8fc4440085d11d795edbd93ae896119 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 02:02:05 -0500 Subject: [PATCH 25/43] fix: move reusable workflow usage to the job level and piece together portions to get things moving possibly --- .github/workflows/merge-demo-feature.yml | 27 +++++++++++++++++++++--- .github/workflows/sync-demos.yml | 15 +++---------- .github/workflows/update-demo.yml | 6 ++++++ 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/.github/workflows/merge-demo-feature.yml b/.github/workflows/merge-demo-feature.yml index 3595ca2..2823edf 100644 --- a/.github/workflows/merge-demo-feature.yml +++ b/.github/workflows/merge-demo-feature.yml @@ -11,6 +11,17 @@ env: ROBUST_MATURIN_DEMO__APP_AUTHOR: ${{ github.repository_owner }} jobs: + update-demo: + name: Update Demo + uses: ./.github/workflows/update-demo.yml + strategy: + matrix: + demo_name: + - "robust-python-demo" + - "robust-maturin-demo" + with: + demo_name: ${{ matrix.demo_name }} + merge-demo-feature: name: Merge Demo Feature runs-on: ubuntu-latest @@ -25,10 +36,20 @@ jobs: with: repository: ${{ github.repository }} - - name: Sync Demo - uses: "./.github/workflows/update-demo.yml" + - name: Checkout Demo + uses: actions/checkout@v4 + with: + repository: "${{ github.repository_owner }}/${{ inputs.demo_name }}" + path: ${{ inputs.demo_name }} + ref: develop + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + + - name: Set up Python + uses: actions/setup-python@v5 with: - demo_name: ${{ matrix.demo_name }} + python-version-file: ".github/workflows/.python-version" - name: Merge Demo Feature PR into Develop working-directory: "${{ github.workspace }}/${{ matrix.demo_name }}" diff --git a/.github/workflows/sync-demos.yml b/.github/workflows/sync-demos.yml index 4477e82..04fcb30 100644 --- a/.github/workflows/sync-demos.yml +++ b/.github/workflows/sync-demos.yml @@ -7,20 +7,11 @@ on: jobs: update-demo: name: Update Demo - runs-on: ubuntu-latest - + uses: ./.github/workflows/update-demo.yml strategy: matrix: demo_name: - "robust-python-demo" - "robust-maturin-demo" - steps: - - name: Checkout Template - uses: actions/checkout@v4 - with: - repository: ${{ github.repository }} - - - name: Update Demo - uses: "./.github/workflows/update-demo.yml" - with: - demo_name: ${{ matrix.demo_name }} + with: + demo_name: ${{ matrix.demo_name }} diff --git a/.github/workflows/update-demo.yml b/.github/workflows/update-demo.yml index 9b971ed..9c6859a 100644 --- a/.github/workflows/update-demo.yml +++ b/.github/workflows/update-demo.yml @@ -16,6 +16,12 @@ jobs: update-demo: runs-on: ubuntu-latest steps: + - name: Checkout Template + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }} + path: "${{ github.workspace }}/cookiecutter-robust-python" + - name: Checkout Demo uses: actions/checkout@v4 with: From 6097e9c848506b67b5f0511718b11f033ab1c978 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 02:05:30 -0500 Subject: [PATCH 26/43] fix: set references to python version file as absolute positions due to multiple checkout oddities arising --- .github/workflows/merge-demo-feature.yml | 2 +- .github/workflows/update-demo.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/merge-demo-feature.yml b/.github/workflows/merge-demo-feature.yml index 2823edf..95cb946 100644 --- a/.github/workflows/merge-demo-feature.yml +++ b/.github/workflows/merge-demo-feature.yml @@ -49,7 +49,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version-file: ".github/workflows/.python-version" + python-version-file: "${{ github.workspace }}/cookiecutter-robust-python/.github/workflows/.python-version" - name: Merge Demo Feature PR into Develop working-directory: "${{ github.workspace }}/${{ matrix.demo_name }}" diff --git a/.github/workflows/update-demo.yml b/.github/workflows/update-demo.yml index 9c6859a..93a933f 100644 --- a/.github/workflows/update-demo.yml +++ b/.github/workflows/update-demo.yml @@ -35,7 +35,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version-file: ".github/workflows/.python-version" + python-version-file: "${{ github.workspace }}/cookiecutter-robust-python/.github/workflows/.python-version" - name: Update Demo working-directory: "${{ github.workspace }}/cookiecutter-robust-python" From 0198495adf4403d5e66ac89671f1e43423eff0c1 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 02:28:10 -0500 Subject: [PATCH 27/43] feat: add small check to gracefully exit when trying to create an existing PR --- scripts/update-demo.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/update-demo.py b/scripts/update-demo.py index b73e7f2..d9a151f 100644 --- a/scripts/update-demo.py +++ b/scripts/update-demo.py @@ -8,6 +8,7 @@ # ] # /// import itertools +import subprocess from pathlib import Path from subprocess import CompletedProcess from typing import Annotated @@ -144,6 +145,10 @@ def _validate_template_main_not_checked_out(branch: str) -> None: def _create_demo_pr(demo_path: Path, branch: str, commit_start: str) -> None: """Creates a PR to merge the given branch into develop.""" gh("repo", "set-default", f"{DEMO.app_author}/{DEMO.app_name}") + search_results: subprocess.CompletedProcess = gh("pr", "list", "--state", "open", "--search", branch) + if "no pull requests match your search" not in search_results.stdout: + typer.secho(f"Skipping PR creation due to existing PR found for branch {branch}") + return body: str = _get_demo_feature_pr_body(demo_path=demo_path, commit_start=commit_start) From bda6c838564b98e54c60e7ef56d3689868974e16 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 02:36:05 -0500 Subject: [PATCH 28/43] fix: swap to nox session install and run script for merge-demo-feature session along with fixing arg passthrough --- noxfile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index 9851cc8..b5c8a3e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -222,11 +222,11 @@ def merge_demo_feature(session: Session, demo: RepoMetadata) -> None: Assumes that all PR's already exist. """ args: list[str] = [*MERGE_DEMO_FEATURE_OPTIONS] + if session.posargs: + args = [*session.posargs, *args] if "maturin" in demo.app_name: args.append("--add-rust-extension") - if session.posargs: - args.extend(session.posargs) - session.run("uv", "run", MERGE_DEMO_FEATURE_SCRIPT, *args) + session.install_and_run_script(MERGE_DEMO_FEATURE_SCRIPT, *args) @nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="setup-release") From 7969d2315cb9a3350f2bf76d47a8d7c51738c517 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 02:41:04 -0500 Subject: [PATCH 29/43] feat: add default option to use current branch and add placeholder default for cache folder for the time until it gets moved to config passthrough later --- scripts/merge-demo-feature.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/merge-demo-feature.py b/scripts/merge-demo-feature.py index c48a395..b084099 100644 --- a/scripts/merge-demo-feature.py +++ b/scripts/merge-demo-feature.py @@ -10,12 +10,14 @@ import subprocess from pathlib import Path from typing import Annotated +from typing import Optional import typer from cookiecutter.utils import work_in from util import DEMO from util import FolderOption +from util import get_current_branch from util import get_demo_name from util import gh @@ -25,13 +27,18 @@ @cli.callback(invoke_without_command=True) def merge_demo_feature( - branch: str, - demos_cache_folder: Annotated[Path, FolderOption("--demos-cache-folder", "-c")], + branch: Optional[str] = None, + demos_cache_folder: Annotated[Path, FolderOption("--demos-cache-folder", "-c")] = None, add_rust_extension: Annotated[bool, typer.Option("--add-rust-extension", "-r")] = False ) -> None: """Searches for the given demo feature branch's PR and merges it if ready.""" demo_name: str = get_demo_name(add_rust_extension=add_rust_extension) + if demos_cache_folder is None: + raise ValueError("Failed to provide a demos cache folder.") + demo_path: Path = demos_cache_folder / demo_name + branch: str = branch if branch is not None else get_current_branch() + with work_in(demo_path): pr_number_query: subprocess.CompletedProcess = gh( "pr", "list", "--head", branch, "--base", DEMO.develop_branch, "--json", "number", "--jq", "'.[0].number'" From e4bf34d6bbed50f5ac68c5d5673c4d8990926093 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 02:43:07 -0500 Subject: [PATCH 30/43] fix: change type of demos_cache_folder to prevent validation error for case not used commonly --- scripts/merge-demo-feature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/merge-demo-feature.py b/scripts/merge-demo-feature.py index b084099..6b94496 100644 --- a/scripts/merge-demo-feature.py +++ b/scripts/merge-demo-feature.py @@ -28,7 +28,7 @@ @cli.callback(invoke_without_command=True) def merge_demo_feature( branch: Optional[str] = None, - demos_cache_folder: Annotated[Path, FolderOption("--demos-cache-folder", "-c")] = None, + demos_cache_folder: Annotated[Optional[Path], FolderOption("--demos-cache-folder", "-c")] = None, add_rust_extension: Annotated[bool, typer.Option("--add-rust-extension", "-r")] = False ) -> None: """Searches for the given demo feature branch's PR and merges it if ready.""" From 1df07441cae4caba6236eaefd3ca2194c2417c63 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 02:48:08 -0500 Subject: [PATCH 31/43] fix: add missing demo env info pass through into nox session install of script --- noxfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index b5c8a3e..9da4dc5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -226,7 +226,9 @@ def merge_demo_feature(session: Session, demo: RepoMetadata) -> None: args = [*session.posargs, *args] if "maturin" in demo.app_name: args.append("--add-rust-extension") - session.install_and_run_script(MERGE_DEMO_FEATURE_SCRIPT, *args) + + demo_env: dict[str, Any] = {f"ROBUST_DEMO__{key.upper()}": value for key, value in asdict(demo).items()} + session.install_and_run_script(MERGE_DEMO_FEATURE_SCRIPT, *args, env=demo_env) @nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="setup-release") From c414c81856d7a2036484e8cfba5a9d2974ad72a8 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 02:49:23 -0500 Subject: [PATCH 32/43] feat: remove unneeded prior install now that PEP 723 is being used --- noxfile.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index 9da4dc5..d1cc803 100644 --- a/noxfile.py +++ b/noxfile.py @@ -195,9 +195,6 @@ def test(session: Session) -> None: ) @nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="update-demo") def update_demo(session: Session, demo: RepoMetadata) -> None: - session.log("Installing script dependencies for updating generated project demos...") - session.install("cookiecutter", "cruft", "platformdirs", "loguru", "python-dotenv", "typer") - session.log("Updating generated project demos...") args: list[str] = [*UPDATE_DEMO_OPTIONS] if "maturin" in demo.app_name: From 9197f2067f0c95f9eb64018bc974e2d6054ab85c Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 02:54:58 -0500 Subject: [PATCH 33/43] fix: remove faulty quotes in jq expression for merge-demo-feature script --- scripts/merge-demo-feature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/merge-demo-feature.py b/scripts/merge-demo-feature.py index 6b94496..0c02798 100644 --- a/scripts/merge-demo-feature.py +++ b/scripts/merge-demo-feature.py @@ -41,7 +41,7 @@ def merge_demo_feature( with work_in(demo_path): pr_number_query: subprocess.CompletedProcess = gh( - "pr", "list", "--head", branch, "--base", DEMO.develop_branch, "--json", "number", "--jq", "'.[0].number'" + "pr", "list", "--head", branch, "--base", DEMO.develop_branch, "--json", "number", "--jq", ".[0].number" ) pr_number: str = pr_number_query.stdout.strip() if pr_number == "": From d5b8fe5b90a23ab2af729484c9e5a342265d3022 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 02:55:34 -0500 Subject: [PATCH 34/43] fix: specify --merge in merge-demo-feature script attempt at merging due to requirement when used in automation --- scripts/merge-demo-feature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/merge-demo-feature.py b/scripts/merge-demo-feature.py index 0c02798..d4f1b42 100644 --- a/scripts/merge-demo-feature.py +++ b/scripts/merge-demo-feature.py @@ -47,7 +47,7 @@ def merge_demo_feature( if pr_number == "": raise ValueError("Failed to find an existing PR from {} to {DEMO.develop_branch}") - gh("pr", "merge", pr_number, "--auto", "--delete-branch") + gh("pr", "merge", pr_number, "--auto", "--delete-branch", "--merge") if __name__ == "__main__": From 90a9ec1eefedf77c5c720d9616508dbff13cb146 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 03:03:23 -0500 Subject: [PATCH 35/43] fix: add missing cruft dependency to PEP 723 block of the setup-release script --- scripts/setup-release.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/setup-release.py b/scripts/setup-release.py index 52ab9f3..ffc7e24 100644 --- a/scripts/setup-release.py +++ b/scripts/setup-release.py @@ -2,6 +2,7 @@ # requires-python = ">=3.10" # dependencies = [ # "cookiecutter", +# "cruft", # "python-dotenv", # "typer", # "tomli>=2.0.0;python_version<'3.11'", From f80069a4ff66d3fae658241e52a55f600c8df49c Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 03:11:29 -0500 Subject: [PATCH 36/43] fix: replace --no-tag with --files-only since that actually exists --- scripts/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/util.py b/scripts/util.py index ee8226c..d09b47d 100644 --- a/scripts/util.py +++ b/scripts/util.py @@ -299,7 +299,7 @@ def bump_version(new_version: str) -> None: Args: new_version: The version to bump to """ - cmd: list[str] = ["uvx", "--from", "commitizen", "cz", "bump", "--changelog", "--yes", "--no-tag", new_version] + cmd: list[str] = ["uvx", "--from", "commitizen", "cz", "bump", "--changelog", "--yes", "--files-only", new_version] # Exit code 1 means 'nothing to bump' - treat as success result: subprocess.CompletedProcess = subprocess.run(cmd, cwd=REPO_FOLDER) if result.returncode not in (0, 1): From d4e0f313f5933b5c021d34e0c969836a57b7413c Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 03:17:33 -0500 Subject: [PATCH 37/43] chore: add a couple of random undesired reference files to the gitignore for the time being --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 40da99f..3c37466 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ nohup.out .env.local /.idea/ +CLAUDE*.md +example.yml +/.claude/* From fba8b5d17e7b20c45aabccd77de176e30d67474c Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 03:20:59 -0500 Subject: [PATCH 38/43] feat: add uv sync call to setup-release script to ensure lockfile gets updated --- scripts/setup-release.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/setup-release.py b/scripts/setup-release.py index ffc7e24..3924d02 100644 --- a/scripts/setup-release.py +++ b/scripts/setup-release.py @@ -24,6 +24,7 @@ from util import git from util import REPO_FOLDER from util import TEMPLATE +from util import uv try: @@ -54,6 +55,8 @@ def main( typer.secho(f"Setting up release: {current_version} -> {new_version}", fg="blue") + ["uv", "sync", "--all-groups"], + uv setup_release(current_version=current_version, new_version=new_version, micro=micro) typer.secho(f"Release branch created: release/{new_version}", fg="green") @@ -102,6 +105,7 @@ def _setup_release(current_version: str, new_version: str, micro: Optional[int] # Sync dependencies typer.secho("Syncing dependencies...", fg="blue") + uv("sync", "--all-groups") git("add", ".") # Create bump commit From 4924dca5a5e1e809564c146835bffbbc0e19b8f6 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 03:21:41 -0500 Subject: [PATCH 39/43] =?UTF-8?q?bump:=20version=202025.11.0=20=E2=86=92?= =?UTF-8?q?=202025.12.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 414 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 416 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..38b2f0f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,414 @@ +## v2025.12.0 (2025-12-01) + +### Feat + +- add uv sync call to setup-release script to ensure lockfile gets updated +- remove unneeded prior install now that PEP 723 is being used +- add default option to use current branch and add placeholder default for cache folder for the time until it gets moved to config passthrough later +- add small check to gracefully exit when trying to create an existing PR +- add check for when the template is already in sync with the demo +- add initial implementation of setup-release nox session and corresponding script prior to review +- add initial implementation of the calendar version release cicd +- move initial template checkout back into the primary workflows to ensure that the reusable workflow exists and is checked out prior to attempting to reference it +- break out demo updates into its own reusable local github action workflow and add in additional syncing for demos with feature branch PR's and general push workflow on develop +- add basedpyright specific configuration based on https://github.com/robust-python/cookiecutter-robust-python/issues/60#issuecomment-3565750469 +- temporarily remove the body generation of the feature to develop PR +- add on automated demo PR creation from feature branches into develop +- alter sync demo process to use ephemeral github commits for generation so that everything is more accurate +- remove unused merge commit check +- add initial attempt at a template level github action to sync demos on PR updates that target develop +- update demo handling to create new feature branches when targeting template feature branches +- improve logic for creating demo releases in github +- add a ton of half finished logic for release handling that will be refactored soon +- update logic in release rollback to not get hung up on faulty cleanup +- add logic for rolling back release creation +- add a check to the setup-release script in generated project along with light refactors +- add PEP 723 syntax installs +- swap to making a new feature branch in demo rather than working from develop always +- rebase branching pattern on develop +- add a prettier ignore file for cookiecutter and cruft generated files +- add python_versions as a cookiecutter derived value and corresponding classifiers +- add placeholder badge examples +- add banner logo in place of old logo +- add a logo to the readme +- update most references to support 3.10 to 3.14 +- remove python-dotenv dependency and loading +- add preparations for moving to org +- add initial generation of some templates +- add placeholders for a template and configuration +- adjust setup-remote to fetch after creation of the remote instead of before +- change the default project name to reflect the repo name +- remove redundant nox tags +- add proper rust caching to github workflows and ensure cargo audit is installed in rust security nox session +- swap set up rust step in lint-rust workflow to use dtolnay/rust-toolchain +- add initial attempt for build-rust workflow +- add build-rust as a nox session +- add repository provider as a part of the testing fixture options and add some basic tests to ensure files are removed properly in the post gen hook +- add in initial rust folder contents with best guess +- add the robust-maturin-demo to the update-demo nox session +- swap to just having one validation function and no intermediary +- add several utility functions and bits of logic to ensure branches are synced before scripts run +- improve lint-from-demo.py in attempt to get it to work smoothly +- condense lint/format commands in bitbucket pipelines and ensure full typecheck matrix gets run +- adjust cicd in gitlab and bitbucket pipelines to ensure parity between platform providers in cicd +- combine various styling nox sessions in cicd +- add a proof of concept bitbucket-pipelines.yml +- ensure the .github folder isn't created when not using github +- add proof of concept gitlab cicd +- adjust base_url in .cz.toml of the generated project to account for cookiecutter value changes +- adapt setup-remote.py to account for the new cookiecutter values +- swap urls that used old github_user cookiecutter value +- swap github_user with more generic options to allow for bitbucket/gitlab compatibility moving forward +- add a readthedocs config for the template +- add a readthedocs config for the cookiecutter +- set docs retention to automatic vs manually 5 days +- replace manual pypi upload through nox session with github action so that oidc publishing can be used +- add testpypi as an index +- remove unused release nox session +- add session args to setup_release nox session to allow for manual increment +- delete explicit build-python.yml workflow and ensure that build-python is called in release-python.yml +- add to the command for getting the release notes so that the header ends up being the new version +- change release-python.yml to operate on push to main or master while also creating a tag and publishing files to the github release draft +- add a portion to release-python.yml to attach package files to the github release draft +- ensure the nox session properly passes through the changelog path +- replace body usage with body_path and make use of github.workspace +- revert back to writing out a body.md file +- ensure v2 is used of action-gh-release +- rename bump-version.yml to prepare-release.yml and rework logic entirely +- remove final job from bump-version.yml to avoid caching the temporary release notes in favor of just drafting the release immediately +- add a get-release-notes.py script and nox session along with adding to the workflows so that a draft release is generated in bump-version.yml +- add in scripts for setting up a release and some partially finished CI/CD components related to it +- add a script and noxfile session for updating demos +- adjust test matrix to not test all python versions on windows and mac +- replace nox venv creation for type checking with uvx +- replace demo_name passthrough with add_rust_extension and generated demo name +- try replacing pyproject.toml with .python-version path from repo root +- add a basic prepare-release.py script and nox session +- remove github workflow check from generated .pre-commit-config.yaml +- remove unneeded venvs from noxfile.py +- remove non-relevant extension recommendation +- add vscode starting extensions and settings +- replace GLOBAL_NOX_SESSIONS with a similar set IDEMPOTENT_NOX_SESSIONS that doesn't have context dependent sessions +- add a pre-commit hook for checking github workflows to the template repo itself +- add a pre-commit hook for checking github workflow files +- add a basic .pre-commit-config.yaml to the template repo itself +- swap to cruft and add basic github actions syntax unit tests +- remove part of the .editorconfig that applies to all files +- add names to any precommit hooks missing them +- replace most of the remote precommit hooks with the same local format that cookiecutter-hypermodern-python used +- add --diff to the ruff-check precommit hook +- add sphinxcontrib-typer as a dev dependency and add it into the docs config +- add license to docs to help fix error +- copy docs for generated project from hypermodern python cookiecutter and clean up template docs config +- convert most low importance nox sessions to use uvx in place of a nox venv +- replace some dev dependency installations with specific smaller dependencies +- add a line to ensure that retrocookie pyproject.toml changes don't get added in +- remove inaccurate nox session portion from the template noxfile.py +- optimize the template's nox sessions to install less unnecessary dependencies +- add a terminal coverage report to the test-python nox session +- add call in in-demo nox session to clear outstanding changes to the demo project prior to running linting/formatting +- try using nox tags for lint and format instead of precommit +- add to the lint-generated-project nox session to try and get it working +- split apart setup-git.py into setup-git.py and setup-remote.py +- add more specific nox sessions to keep tags accurate and consolidate groups in pyproject.toml +- add prettier to the .pre-commit-config.yaml +- improve git setup logic +- test out using tags for nox sessions +- change generate-demo-project.py to regenerate demos on top of existing ones unless --no-cache is provided +- add a bunch of safety logic to generate-demo-project.py to avoid something bad happening with shutil rmtree on accident +- add new scripts in scripts folder and move some of the initial setup logic into them +- expand and refactor integration tests for the template +- ensure rust session aren't created if not using maturin and add additional compatibility sessions for lint, publish, etc +- copy .editorconfig from the inner portion to the template +- update commitizen version in .pre-commit-config.yaml +- add commitizen pre-commit hook and edit settings +- remove execution environments from pyrightconfig.json to allow for proper import resolution +- add a DEBUG flag to pyrightconfig.json +- remove .cruft.json from .gitignore +- add hypermodern cookiecutter logic for installing pre-commit-hooks +- remove .cruft.json from the .gitignore +- add pre-commit migration change +- remove non-existent installation group +- removes final uv sync calls +- convert leftover uv sync calls in noxfile.py to install normally +- convert all manual uv executions in nox to just implicitly use it through the venv +- yet another attempt at fixing jinja madness +- attempt to fix jinja escaping +- add target and .idea to .gitignore +- more changes hoping to get release-python.yml working +- attempt at fixing syntax error for release-python.yml +- update version of ruff used in pre-commit hooks +- runs ruff format and check on the template level and adds a few small logic tweaks +- adjust syntax of token in bump-version.yml +- add alternate branch of master for all on push jobs +- change dependabot.yml branch target to develop +- add all generated egg-info folders to template gitignore +- add .ruff.toml for the template itself +- add some pytests for the template itself to see if this is a helpful pattern or not +- add typer as a default dependency +- add the start of the templates testing architecture +- add some new utility sessions to the template's noxfile +- add formatting to lint-python.yml +- remove unneeded docs-build-rust.yml +- remove unneeded docs-build-python.yml +- remove uv.lock from project generator since it contains package specific meta info +- adjust session names in noxfile +- remove build_rust for the meantime while figuring out how to do it separate of maturin's build +- adjust names of sessions in noxfile and various aspects of its functioning +- add fixes using retrocookie +- add retrocookie as a dependency +- add basic glossary +- add missing topics to docs +- add in post_gen_project.py from hypermodern cookiecutter hooks +- add syncing generated project to existing uv.lock +- update sync-uv-with-demo.py to use typer and uv +- add .gitignore for the template itself +- add several expected dependencies for the template itself +- copy dependency groups from inner project's pyproject.toml +- add basic non-package pyproject.toml for the template itself +- add poc publish_rust nox session +- bulk initial commit + +### Fix + +- replace --no-tag with --files-only since that actually exists +- add missing cruft dependency to PEP 723 block of the setup-release script +- specify --merge in merge-demo-feature script attempt at merging due to requirement when used in automation +- remove faulty quotes in jq expression for merge-demo-feature script +- add missing demo env info pass through into nox session install of script +- change type of demos_cache_folder to prevent validation error for case not used commonly +- swap to nox session install and run script for merge-demo-feature session along with fixing arg passthrough +- set references to python version file as absolute positions due to multiple checkout oddities arising +- move reusable workflow usage to the job level and piece together portions to get things moving possibly +- tweak gitlab ci to not error from too many uv cache keyfiles being defined +- change paths pointing toward update-demo reusable workflow +- remove github.workspace from uses path +- update names throughout nox session and file along with fix update-demo not creating branches and some env var issues +- replace uv run call in update-demo nox session with install_and_run_script +- remove faulty import +- adjust formatting to avoid initial lint error +- update the location of where the update-demo.yml reusable workflow is searched for to be in the subdirectory that is actually being checked out into +- add absolute prefix to custom checkout locations in hopes of fixing issue with reusable action not being found in CICD +- remove manual specification of venv and venvpath in hopes it works for cicd +- add adaptable python executable path to basedpyright usage in nox session +- remove faulty configuration options +- replace pyright call with basedpyright +- add old env var for project cache to sync-demos.yml for the time being +- replace matrix values in sync-demo to account for previous simplification in internal nox session +- add working-directory kwarg to sync-demo so that the nox command is run inside the repo +- alter kwarg chaining in hopes of fixing error +- update key used to find commit info in cruft json +- remove old code from update-demo that is no longer compatible +- remove invalid pass through attempts of text=true throughout scripts +- add interop layer between older noxfile methods and newer env var logic for the time being +- remove unintended broken snippet +- replace broken import in scripts +- remove faulty syntax +- ensure that pushing a new branch for the first time works +- add a few workarounds trying to get POC branching going before refactoring +- remove accidentally added import +- swap is_ancestor to use its own error handling due to git merge-base --is-ancestor only showing through status +- ensure that pushing a new branch for the first time works +- add a few workarounds trying to get POC branching going before refactoring +- remove accidentally added import +- swap is_ancestor to use its own error handling due to git merge-base --is-ancestor only showing through status +- add env var to noxfile so that latest abi is used by default if not known +- replace list comprehension with loop in pre template generation hook +- moves jinja logic into docstring because apparently that matters somehow +- replace old loop with jinja plus assignment separate +- temporarily remove for loops to find issue +- replace sync with lock to avoid hanging issue +- add a step to the update-demo script to ensure python is pinned correctly, installed, and synced when changed +- add some temp defaults to enable updating demos across python versions used +- replace references meant to be latest python with 3.14 if missed in first go around +- adjust reference to nox session +- adjust cicd workflow tests to properly parse for relevant portions +- add to the pytest config so that it doesn't try to test the generated tests portion +- adjust broken import links caused by test sources root not being treated like a viable import path +- add missing platformdirs dependency to cookiecutter templates PEP 723 syntax +- add missing nox dependency in PEP 723 syntax +- add python-dotenv dependency to the noxfile through PEP 723 syntax +- remove no longer used load_dotenv +- add some missing nox session installs for python-dotenv and refactor names to reduce entropy +- replace myst-parser with myst-parser[linkify] in the cookiecutter docs requirements.txt +- swap around git initialization and venv creation so that the lock file gets committed in the initial commit +- swap around setup-git so that the develop branch gets made properly +- add uvx prior to maturin call in nox session build-python so that it installs if needed +- add missing pyo3 feature `extension-module` and remove Cargo.lock from template due to incongruencies between generated from template vs cargo +- update rust-cache usage to the post 2.0.0 parameter key "workspaces" instead of "workdir" +- replace license-files.paths with just license-files and set minimum maturin version to 1.9.0 +- adjust nox sessions for rust formatting and linting to actually install dependencies +- replace typo in Cargo.toml that was breaking the syntax +- attempt to fix license-files syntax +- adjust license-files in pyproject.toml to satisfy pep 639 and maturin +- escape values in test-rust.yml where needed +- replace all faulty checks against "y" for add_rust_extension with checking for true +- remove -h option from setup-remote.py to ensure no conflicting cli inputs +- adjust precommit to match nox file version +- add step to ensure develop is checked out before updating the demo +- adjust invalid path imports +- attempt to adjust retrocookie usage in lint-from-demo.py +- add step to checkout develop prior to deleting old lint-from-demo branch +- adjust ignored file paths to be post template insertion values +- adjust path to bitbucket-pipelines.yml in post_gen_project.py +- attempt to have the paths actually remove +- attempting to get post gen to properly run +- adjust post_gen_project.py main block to call the correct function +- adjust conditionals in post_gen_project.py to include quotes where missing +- adjust post_gen_hook to properly target repository_provider not platform_provider +- fix files checked in cicd +- ensure lock file updates when running setup-release.py in generated project +- add missing main block +- adjust session decorator kwargs throughout template noxfile +- remove non existent --draft kwarg from gh release upload command in release-python.yml +- add v prefix to the tag_name provided to the release draft creation in prepare-release.yml +- add --clobber and --draft to the github release file attachment to the drafted release +- replace v addition to tag creation with v addition in get_tag job +- add v prefix to git tag command +- adjust names of github actions steps and ensure we provide a glob path to the github release file attachment +- add missing permissions for release-python.yml create tag step +- add missing oidc testpypi and pypi upload permissions +- replace twine env variables with uv versions +- replace --repository with --index in testpypi publish step +- adjust nox session command in testpypi publish step +- ensure code is checked out and the built package is properly named in release-python.yml +- add a fetch-depth of 0 and fetch-tags as true to prepare-release.yml so that commitizen works properly +- add base64 encode and decode step to the changelog output to ensure multiline isn't an issue +- replace body_path with body in the create release draft step of prepare-release.yml +- replace nox usage for get-release-notes with the python script to have a clean stdout output with no logs +- add missing syntax escaping for cookiecutter +- add a step to get the version from branch name and then use that for the release draft +- remove tag_name in bump-version.yml due to env var being used not existing +- add write permissions to allow for the gh release to work +- add missing nox command portion in bump-version.yml +- add missing --no-verify to automated commit +- ensure a rev_range isn't passed to cz changelog if there hasn't been a release yet +- replace --with with --from +- add missing section to get_package_version command +- add missing capture_output flag to get_latest_release_notes +- adjust any uvx cz commands +- adjust git command in release branch command +- replace --with with --from +- add --with commitizen to uvx cz call +- remove cz from dependencies check due to command difference +- remove type alias to avoid needing typing_extensions +- replace python=None with False in nox file along adding the option to setup a certain release increment +- adjust setup-release.py argparse usage +- adjust import to backport in util.py +- add main to branches list for bump-version.yml +- adjust workflow path reference in build-python.yml so that it actually gets called when the workflow file is updated +- fix path syntax in build-python.yml +- adjust nox session name in typecheck-python.yml to match old format +- revert typecheck nox session back to creating a venv due to apparently needing dependencies +- add quotes around nox session call +- replace args passed to update-demo.py script with altered args +- remove non-existent kwarg from cruft update call and fix type of template_path +- adjust kwargs passed to cruft update +- add missing dependencies for the update-demo nox session +- add missing python arg to update-demo nox session +- adjust typecheck-python.yml to match changes to nox session +- adjust name of used nox session in typecheck-python.yml to match the nox session in its usage +- remove unused github actions workflow and fix nox session names in other workflows +- adjust path to python-version-file for most github actions workflows +- adjust nox session name in build-python.yml +- replace .python-version with "pyproject.toml" for all workflows that just need a compatible python version and just want latest +- ensure the test-python.yml github workflow only runs the needed python version +- add missing double quotes to .cz.toml +- adjust commitizen version handling +- add version to .cz.toml +- adjust integration tests to account for previous fixture changes +- remove indirect parametrization from a few sub fixtures of robust_demo and ensure its name changes as needed based on permutations +- fix syntax in release-python.yml and remove junk comments +- adjust utility test function to properly output relative paths not absolute +- fix escaping in build-python.yml +- adjust quotes and escaping in bump-version.yml +- fix the section tag for yml files in the .editorconfig +- remove manual hook stage from nox precommit session +- remove no longer used darglint hook +- remove redundant ruff calls +- update stages in precommit hooks to account for name deprecations +- replace ruff with ruff-check in .pre-commit-config.yaml +- add missing titles to copied over docs +- attempt to improve and get docs configs working +- adjust --outdir to --out-dir in the build_python nox session +- remove session install calls from nox sessions that don't need a venv +- fix usage of check_returncode in test_demo_project_nox_session +- adjust output_dir to be correct +- adjust invalid fixture references +- adjust typers in parametrize calls +- adjust forgotten names in fixtures +- adjust name of python test nox session in GLOBAL_NOX_SESSIONS +- add missing list definition syntax in pyproject.toml +- move tool.maturin section in pyproject.toml to the correct block +- adjust type faulty import +- copy over signature from generate-demo-project.py to match-generated-precommit.py +- adjust Generator type annotation +- adjust invalid import +- add -a to the git commit in the lint-generated-project nox session +- add several portions to get the lint-generated-project nox session to work +- remove non existent tag from nox session +- adjust some jinja escaping to properly work in github action workflows +- adjust test sessions to account for noxfile changes +- adjust syntax in setup-git.py +- adjust generate-demo-project.py to only remove a demo of the same name to avoid some really bad situations with shutil rmtree +- add a Path wrapper around PROJECT_DEMOS_FOLDER in noxfile.py +- use os.getenv to get environment variable in the template noxfile.py +- swap maturin and uv build locations in the build-python nox session +- remove happenstance retrocookie insertion into nox version specification +- remove faulty kwarg from nox session security-python +- adjust bandit.yml path in nox session security-python +- remove invalid kwarg from nox session security-python +- ensure package metadata is passed correctly and fix syntax errors in release-python.yml +- correct arg error in precommit hook and adjust max size of files to 2000 kb +- adjust path to .ruff.toml in .pre-commit-config.yaml +- remove uv run from template lint ruff calls +- replace manual uv sync with session install +- add missing group kwargs +- remove faulty command kwargs from noxfile.py +- adjust syntax in .cz.toml +- move tool.maturin into the block with its contents +- alter syntax in various places so that docs build successfully for the first time +- adjust tons of doc links to actually work +- remove invalid and some unused myst extensions +- update dependencies and move them into proper groups +- alter several invalid uv sync commands +- remove non-existent kwarg from uv sync commands in noxfile.py +- adjust cookiecutter variable names throughout so that the template can generate correctly +- convert generate-demo-project.py to typer +- change generate-demo-project.py to use typer and adjust noxfile accordingly +- adjust max_python_version to the latest stable +- update pyproject.toml to reference actual cookiecutter variables +- remove pointless default nox session that didn't exist + +### Refactor + +- move tag-version into its own script for the time being +- move get_current_version into bump-version script to avoid unneeded tomli install in other scripts +- rename script function to reflect usage only occurring in demo +- move constants to module level in places +- break apart util function for validating branch history and state +- make git related commands more accurate +- replace any remaining comments or references to personal github with org +- replace references to personal info with org info +- replace references to `56kyleoliver@gmail.com` with `cookiecutter.robust.python@gmail.com` +- replace all instances of `56kyle/cookiecutter-robust-python` with `robust-python/cookiecutter-robust-python` +- adjust import order in setup-release.py to get the robust-python-demo's lint cicd to pass +- adjust formatting to appease ruff in generated results +- small tweaks to gitlab cicd for rust jobs +- move some script checks and clean up into the uv-cache anchor +- rename the rust github workflows to their expected values and have them removed if needed in the post gen project hook +- give in to prettier for the moment just to see if this can start working +- run lint-from-demo.py and manually merge certain portions +- ensure .readthedocs.yml is only used with bitbucket +- rename docs-build.yml to build-docs.yml to better follow the existing verb-type naming pattern that the other github actions follow +- replace cli pass through of repo path with a constant in the scripts util folder for the template repo +- rearrange parametrization order in test_github_workflows.py to better display test values +- move the release nox session before the publish-python and publish-rust nox sessions in hopes that the release tag session will be ordered correctly +- replace various single quoted values in github workflows with double quotes +- shorten generate-demo-project.py to just generate-demo.py along with any related variables / etc +- move junit test results to tests/results +- rename match-generated-precommit.py to lint-from-demo.py and any other similarly named variables +- adjust the format of MissingDependencyError message creation +- break apart integration test fixtures into more parametrized and modular portions +- move activate_virtualenv_in_precommit_hooks to the end of the noxfile for clarity's sake +- clean up the docs conf.py file diff --git a/pyproject.toml b/pyproject.toml index 5d42991..ada6c7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cookiecutter-robust-python" -version = "2025.11.0" +version = "2025.12.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.10,<4.0" diff --git a/uv.lock b/uv.lock index 32deb56..aa6e894 100644 --- a/uv.lock +++ b/uv.lock @@ -356,7 +356,7 @@ wheels = [ [[package]] name = "cookiecutter-robust-python" -version = "2025.11.0" +version = "2025.12.0" source = { virtual = "." } dependencies = [ { name = "cookiecutter" }, From 7403db475b0bf4ddf4dbaf59b74bdbde83a9894b Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 03:27:15 -0500 Subject: [PATCH 40/43] fix: remove accidentally left in broken syntax --- scripts/setup-release.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/setup-release.py b/scripts/setup-release.py index 3924d02..6207c46 100644 --- a/scripts/setup-release.py +++ b/scripts/setup-release.py @@ -55,8 +55,6 @@ def main( typer.secho(f"Setting up release: {current_version} -> {new_version}", fg="blue") - ["uv", "sync", "--all-groups"], - uv setup_release(current_version=current_version, new_version=new_version, micro=micro) typer.secho(f"Release branch created: release/{new_version}", fg="green") From d9c3de6303d160161fb0c330a9a2e164291c3003 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 03:28:41 -0500 Subject: [PATCH 41/43] fix: replace get-release-notes nox session run of script with install and run --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index d1cc803..ec6d2e7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -303,7 +303,7 @@ def get_release_notes(session: Session) -> None: nox -s get-release-notes # Write to release_notes.md nox -s get-release-notes -- /path/to/file.md # Write to custom path """ - session.run("python", GET_RELEASE_NOTES_SCRIPT, *session.posargs, external=True) + session.install_and_run_script(GET_RELEASE_NOTES_SCRIPT, *session.posargs) @nox.session(python=False, name="remove-demo-release") From 229f005ff2b6e0b05ec27bf8a9127cb5ab2616ef Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 03:29:41 -0500 Subject: [PATCH 42/43] fix: add venv for get-release-notes for time being --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index ec6d2e7..b6928be 100644 --- a/noxfile.py +++ b/noxfile.py @@ -295,7 +295,7 @@ def tag_version(session: Session) -> None: session.run("python", TAG_VERSION_SCRIPT, *args, external=True) -@nox.session(python=False, name="get-release-notes") +@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="get-release-notes") def get_release_notes(session: Session) -> None: """Extract release notes for the current version. From a31b5aaf1ef55e64c7e00e7f20ce90b571b426a4 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Mon, 1 Dec 2025 03:31:40 -0500 Subject: [PATCH 43/43] fix: add missing dependencies to get-release-notes script --- scripts/get-release-notes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/get-release-notes.py b/scripts/get-release-notes.py index 3d982c2..a0af5a9 100644 --- a/scripts/get-release-notes.py +++ b/scripts/get-release-notes.py @@ -1,6 +1,8 @@ # /// script # requires-python = ">=3.10" # dependencies = [ +# "cookiecutter", +# "cruft", # "python-dotenv", # "typer", # ]