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/merge-demo-feature.yml b/.github/workflows/merge-demo-feature.yml new file mode 100644 index 0000000..95cb946 --- /dev/null +++ b/.github/workflows/merge-demo-feature.yml @@ -0,0 +1,56 @@ +name: merge-demo-feature.yml +on: + push: + 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 + 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 + strategy: + matrix: + demo_name: + - "robust-python-demo" + - "robust-maturin-demo" + steps: + - name: Checkout Template + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }} + + - 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.workspace }}/cookiecutter-robust-python/.github/workflows/.python-version" + + - 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/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/.github/workflows/sync-demos.yml b/.github/workflows/sync-demos.yml index abeab83..04fcb30 100644 --- a/.github/workflows/sync-demos.yml +++ b/.github/workflows/sync-demos.yml @@ -4,45 +4,14 @@ on: branches: - 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 }} - ROBUST_MATURIN_DEMO__APP_AUTHOR: ${{ github.repository_owner }} - 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 }} - 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 }}" + with: + 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..93a933f --- /dev/null +++ b/.github/workflows/update-demo.yml @@ -0,0 +1,42 @@ +name: update-demo.yml +on: + workflow_call: + inputs: + demo_name: + 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: "${{ github.workspace }}/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.workspace }}/cookiecutter-robust-python/.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/.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/* 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/CONTRIBUTING.md b/CONTRIBUTING.md index 5dca0c8..106a087 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 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). - ---- diff --git a/noxfile.py b/noxfile.py index 33d7d3d..b6928be 100644 --- a/noxfile.py +++ b/noxfile.py @@ -14,7 +14,6 @@ import nox import platformdirs from dotenv import load_dotenv -from nox.command import CommandFailed from nox.sessions import Session @@ -26,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" @@ -37,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, @@ -46,16 +44,17 @@ ) ).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 -)).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() DEFAULT_DEMO_NAME: str = "robust-python-demo" -DEMO_ROOT_FOLDER: Path = PROJECT_DEMOS_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" @@ -68,6 +67,14 @@ *("--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 + +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" + @dataclass class RepoMetadata: @@ -118,7 +125,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.") @@ -188,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: @@ -200,44 +204,106 @@ 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.install_and_run_script(UPDATE_DEMO_SCRIPT, *args, env=demo_env) -@nox.session(python=False, name="release-template") -def release_template(session: Session): - """Run the release process for the TEMPLATE using Commitizen. +@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. - 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 '--'. + Assumes that all PR's already exist. """ - 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", - ) + 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") + + 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) - cz_bump_args = ["uvx", "cz", "bump", "--changelog"] - if increment: - cz_bump_args.append(f"--increment={increment}") +@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. - 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) + 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) - 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.") + +@nox.session(python=False, name="bump-version") +def bump_version(session: Session) -> None: + """Bump version using CalVer (YYYY.MM.MICRO). + + Usage: + nox -s bump-version # Auto-increment micro for current month + nox -s bump-version -- 5 # Force micro version to 5 + """ + 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) + + 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) + + +@nox.session(python=False, name="tag-version") +def tag_version(session: Session) -> None: + """Create and push a git tag for the current version. + + Usage: + nox -s tag-version # Create tag locally + nox -s tag-version -- push # Create and push tag + """ + args: list[str] = ["--push"] if "push" in session.posargs else [] + session.run("python", TAG_VERSION_SCRIPT, *args, external=True) + + +@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. + + 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.install_and_run_script(GET_RELEASE_NOTES_SCRIPT, *session.posargs) @nox.session(python=False, name="remove-demo-release") @@ -245,4 +311,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/pyproject.toml b/pyproject.toml index 1ce0604..ada6c7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cookiecutter-robust-python" -version = "0.1.0" +version = "2025.12.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..1d17192 --- /dev/null +++ b/scripts/bump-version.py @@ -0,0 +1,66 @@ +# /// 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 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 REPO_FOLDER + + +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: + """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) + + +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/get-release-notes.py b/scripts/get-release-notes.py new file mode 100644 index 0000000..a0af5a9 --- /dev/null +++ b/scripts/get-release-notes.py @@ -0,0 +1,50 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "cookiecutter", +# "cruft", +# "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/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/merge-demo-feature.py b/scripts/merge-demo-feature.py new file mode 100644 index 0000000..d4f1b42 --- /dev/null +++ b/scripts/merge-demo-feature.py @@ -0,0 +1,54 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "cookiecutter", +# "cruft", +# "python-dotenv", +# "typer", +# ] +# /// +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 + + +cli: typer.Typer = typer.Typer() + + +@cli.callback(invoke_without_command=True) +def merge_demo_feature( + branch: Optional[str] = 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.""" + 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" + ) + 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", "--merge") + + +if __name__ == "__main__": + cli() diff --git a/scripts/setup-release.py b/scripts/setup-release.py new file mode 100644 index 0000000..6207c46 --- /dev/null +++ b/scripts/setup-release.py @@ -0,0 +1,130 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "cookiecutter", +# "cruft", +# "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 +from util import uv + + +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") + uv("sync", "--all-groups") + 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() 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() diff --git a/scripts/update-demo.py b/scripts/update-demo.py index a300991..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 @@ -51,16 +52,22 @@ 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 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 " @@ -69,6 +76,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 +94,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: @@ -135,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) diff --git a/scripts/util.py b/scripts/util.py index 2844afb..d09b47d 100644 --- a/scripts/util.py +++ b/scripts/util.py @@ -248,3 +248,98 @@ 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 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", "--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): + 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 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") diff --git a/uv.lock b/uv.lock index 7e409a7..aa6e894 100644 --- a/uv.lock +++ b/uv.lock @@ -356,7 +356,7 @@ wheels = [ [[package]] name = "cookiecutter-robust-python" -version = "0.1.0" +version = "2025.12.0" source = { virtual = "." } dependencies = [ { name = "cookiecutter" }, 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 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: