diff --git a/.cookiecutter.json b/.cookiecutter.json index ba278af..7fbc729 100644 --- a/.cookiecutter.json +++ b/.cookiecutter.json @@ -1,5 +1,5 @@ { - "_commit": "dab1f8da6cb6be90a50db1aafb4411ec61fbcb2c", + "_commit": "e75916b0cb7d49cc8b12acaf0af9098ec3ccca28", "_template": "C:\\Users\\56kyl\\source\\repos\\cookiecutter-robust-python", "add_rust_extension": false, "author": "Kyle Oliver", diff --git a/.cruft.json b/.cruft.json index 2dab440..b2e1bf5 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "C:\\Users\\56kyl\\source\\repos\\cookiecutter-robust-python", - "commit": "dab1f8da6cb6be90a50db1aafb4411ec61fbcb2c", + "commit": "e75916b0cb7d49cc8b12acaf0af9098ec3ccca28", "checkout": null, "context": { "cookiecutter": { @@ -18,7 +18,7 @@ "license": "MIT", "development_status": "Development Status :: 1 - Planning", "_template": "C:\\Users\\56kyl\\source\\repos\\cookiecutter-robust-python", - "_commit": "dab1f8da6cb6be90a50db1aafb4411ec61fbcb2c" + "_commit": "e75916b0cb7d49cc8b12acaf0af9098ec3ccca28" } }, "directory": null diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml deleted file mode 100644 index 63ad4f4..0000000 --- a/.github/workflows/bump-version.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Bump version - -on: - push: - branches: - - master - - main - -jobs: - bump-version: - if: "!startsWith(github.event.head_commit.message, 'bump:')" - runs-on: ubuntu-latest - name: "Bump version and create changelog with commitizen" - steps: - - name: Check out - uses: actions/checkout@v4 - with: - token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - fetch-depth: 0 - - name: Create bump and changelog - uses: commitizen-tools/commitizen-action@master - with: - github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - changelog_increment_filename: body.md - - name: Release - uses: softprops/action-gh-release@v1 - with: - body_path: "body.md" - tag_name: ${{ env.REVISION }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..d380342 --- /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: ${{ steps.new_version.outputs.NEW_VERSION }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml index 8c99c07..5777997 100644 --- a/.github/workflows/release-python.yml +++ b/.github/workflows/release-python.yml @@ -5,21 +5,41 @@ name: Release Python Package on: push: - tags: - - "v[0-9]+.[0-9]+.[0-9]+" - - "v[0-9]+.[0-9]+.[0-9]+-*" # Include pre-release tags + branches: + - main + - master workflow_dispatch: - inputs: - tag: - description: "Git tag to build and release (e.g., v1.2.3). Must already exist." - required: true jobs: + get_tag: + name: Get Tag + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.current_version.outputs.CURRENT_VERSION }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - 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 + + build_and_testpypi: name: Build & Publish to TestPyPI runs-on: ubuntu-latest - + needs: get_tag + outputs: + tag: ${{ needs.get_tag.outputs.tag }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -44,7 +64,7 @@ jobs: - name: Download built package artifacts uses: actions/download-artifact@v4 with: - name: distribution-packages-${{ github.event.inputs.tag }} + name: distribution-packages-${{ needs.get_tag.outputs.tag }} path: dist/ - name: Publish to TestPyPI @@ -53,24 +73,11 @@ jobs: TWINE_PASSWORD: ${{ secrets.TESTPYPI_API_TOKEN }} run: uvx nox -s publish-package -- --repository testpypi - - name: Get Release Notes from Changelog - id: changelog - uses: simple-changelog/action@v3 - with: - path: CHANGELOG.md - tag: ${{ github.event_name == 'push' && github.ref_name || github.event.inputs.tag }} - - outputs: - changelog_body: - description: "Release notes body extracted from CHANGELOG.md" - value: ${{ steps.changelog.outputs.changes }} # Output the extracted changelog body - - publish_pypi: - name: Publish to Production PyPI + publish_pypi_and_github: + name: Publish to Production PyPI and GitHub runs-on: ubuntu-latest needs: build_and_testpypi - if: "github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')" steps: - name: Download package artifacts uses: actions/download-artifact@v4 @@ -86,34 +93,18 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 + - name: Create Tag + run: | + git tag ${{ needs.build_and_testpypi.outputs.tag }} + git push origin ${{ needs.build_and_testpypi.outputs.tag }} + - name: Publish to PyPI env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: uvx nox -s publish-python - create_github_release: - name: Create GitHub Release - runs-on: ubuntu-latest - needs: build_and_testpypi - - if: "github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')" - - steps: - - name: Download package artifacts # Get built artifacts for release assets - uses: actions/download-artifact@v4 - with: - name: distribution-packages - - - name: Get tag name - id: get_tag - run: echo "tag=${{ github.ref_name }}" >> $GITHUB_OUTPUT - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.get_tag.outputs.tag }} - name: Release ${{ steps.get_tag.outputs.tag }} - body: ${{ needs.build_and_testpypi.outputs.changelog_body }} - files: dist/* - prerelease: ${{ contains(steps.get_tag.outputs.tag, '-') }} # Checks if tag contains hyphen (e.g. v1.0.0-rc.1) + - name: Publish to GitHub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload ${{ needs.build_and_testpypi.outputs.tag }} dist/ diff --git a/.gitignore b/.gitignore index 9508269..9fced39 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,6 @@ nohup.out # Prettier dependencies node_modules/ + +# Release Notes +body.md diff --git a/.ruff.toml b/.ruff.toml index d932030..2ab3056 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -98,7 +98,7 @@ max-complexity = 10 ] "exceptions.py" = ["D107"] "noxfile.py" = ["S101"] -"scripts/*" = ["S603"] +"scripts/*" = ["S603", "S607"] [lint.pydocstyle] convention = "google" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..52aea68 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +## v0.1.0 (2025-07-16) + +### Feat + +- initial commit +- remove unneeded python venvs from noxfile.py +- run pre-commit autoupdate +- initial commit +- initial commit +- initial commit +- add new pre-commit hooks for validating the pyproject.toml +- initial commit +- initial commit +- initial commit +- initial commit +- initial commit +- initial commit +- initial commit + +### Fix + +- resolve conflicts +- resolve conflicts +- fix path syntax in build-python.yml diff --git a/build/lib/robust_python_demo/__init__.py b/build/lib/robust_python_demo/__init__.py deleted file mode 100644 index 40f1b5d..0000000 --- a/build/lib/robust_python_demo/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Robust Python Demo.""" diff --git a/build/lib/robust_python_demo/__main__.py b/build/lib/robust_python_demo/__main__.py deleted file mode 100644 index 83d387b..0000000 --- a/build/lib/robust_python_demo/__main__.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Command-line interface.""" - -import typer - - -app: typer.Typer = typer.Typer() - - -@app.command(name="robust-python-demo") -def main() -> None: - """Robust Python Demo.""" - - -if __name__ == "__main__": - app() # pragma: no cover diff --git a/build/lib/robust_python_demo/py.typed b/build/lib/robust_python_demo/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/noxfile.py b/noxfile.py index 6aa7f13..6604314 100644 --- a/noxfile.py +++ b/noxfile.py @@ -47,13 +47,13 @@ RUST: str = "rust" -@nox.session(python=None, name="setup-git", tags=[ENV]) +@nox.session(python=False, name="setup-git", tags=[ENV]) def setup_git(session: Session) -> None: """Set up the git repo for the current project.""" session.run("python", SCRIPTS_FOLDER / "setup-git.py", REPO_ROOT, external=True) -@nox.session(python=None, name="setup-venv", tags=[ENV]) +@nox.session(python=False, name="setup-venv", tags=[ENV]) def setup_venv(session: Session) -> None: """Set up the virtual environment for the current project.""" session.run("python", SCRIPTS_FOLDER / "setup-venv.py", REPO_ROOT, "-p", PYTHON_VERSIONS[0], external=True) @@ -72,14 +72,14 @@ def precommit(session: Session) -> None: activate_virtualenv_in_precommit_hooks(session) -@nox.session(python=None, name="format-python", tags=[FORMAT, PYTHON]) +@nox.session(python=False, name="format-python", tags=[FORMAT, PYTHON]) def format_python(session: Session) -> None: """Run Python code formatter (Ruff format).""" session.log(f"Running Ruff formatter check with py{session.python}.") session.run("uvx", "ruff", "format", *session.posargs) -@nox.session(python=None, name="lint-python", tags=[LINT, PYTHON]) +@nox.session(python=False, name="lint-python", tags=[LINT, PYTHON]) def lint_python(session: Session) -> None: """Run Python code linters (Ruff check, Pydocstyle rules).""" session.log(f"Running Ruff check with py{session.python}.") @@ -96,7 +96,7 @@ def typecheck(session: Session) -> None: session.run("pyright", "--pythonversion", session.python) -@nox.session(python=None, name="security-python", tags=[SECURITY, PYTHON, CI]) +@nox.session(python=False, name="security-python", tags=[SECURITY, PYTHON, CI]) def security_python(session: Session) -> None: """Run code security checks (Bandit) on Python code.""" session.log(f"Running Bandit static security analysis with py{session.python}.") @@ -144,7 +144,7 @@ def docs_build(session: Session) -> None: session.run("sphinx-build", "-b", "html", "docs", str(docs_build_dir), "-W") -@nox.session(python=None, name="build-python", tags=[BUILD, PYTHON]) +@nox.session(python=False, name="build-python", tags=[BUILD, PYTHON]) def build_python(session: Session) -> None: """Build sdist and wheel packages (uv build).""" session.log(f"Building sdist and wheel packages with py{session.python}.") @@ -154,7 +154,7 @@ def build_python(session: Session) -> None: session.log(f"- {path.name}") -@nox.session(python=None, name="build-container", tags=[BUILD]) +@nox.session(python=False, name="build-container", tags=[BUILD]) def build_container(session: Session) -> None: """Build the Docker container image. @@ -193,18 +193,25 @@ def build_container(session: Session) -> None: session.log(f"Container image {project_image_name}:latest built locally.") -@nox.session(python=None, name="prepare-release", tags=[RELEASE]) -def prepare_release(session: Session) -> None: +@nox.session(python=False, name="setup-release", tags=[RELEASE]) +def setup_release(session: Session) -> None: """Prepares a release by creating a release branch and bumping the version. - Does not commit or push the release branch. Additionally, does not tag the release. + Additionally, creates the initial bump commit but doesn't push it. """ - session.log("Preparing release...") + session.log("Setting up release...") - session.run("python", SCRIPTS_FOLDER / "prepare-release.py", external=True) + session.run("python", SCRIPTS_FOLDER / "setup-release.py", external=True) -@nox.session(python=None, tags=[RELEASE]) +@nox.session(python=False, name="get-release-notes", tags=[RELEASE]) +def get_release_notes(session: Session) -> None: + """Gets the latest release notes if between bumping the version and tagging the release.""" + session.log("Getting release notes...") + session.run("python", SCRIPTS_FOLDER / "get-release-notes.py", *session.posargs, external=True) + + +@nox.session(python=False, tags=[RELEASE]) def release(session: Session) -> None: """Run the release process using Commitizen. @@ -239,7 +246,7 @@ def release(session: Session) -> None: session.log("IMPORTANT: Push commits and tags to remote (`git push --follow-tags`) to trigger CD pipeline.") -@nox.session(python=None, name="publish-python", tags=[RELEASE]) +@nox.session(python=False, name="publish-python", tags=[RELEASE]) def publish_python(session: Session) -> None: """Publish sdist and wheel packages to PyPI via uv publish. @@ -253,7 +260,7 @@ def publish_python(session: Session) -> None: session.run("uv", "publish", "dist/*", *session.posargs, external=True) -@nox.session(python=None) +@nox.session(python=False) def tox(session: Session) -> None: """Run the 'tox' test matrix. diff --git a/pyproject.toml b/pyproject.toml index c644ea1..3a8d4d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "robust-python-demo" -version = "0.0.0" +version = "0.1.0" description = "robust-python-demo" authors = [ { name = "Kyle Oliver", email = "56kyleoliver+cookiecutter-robust-python@gmail.com" }, @@ -19,7 +19,8 @@ classifiers = [ dependencies = [ "loguru>=0.7.3", "platformdirs>=4.3.8", - "typer>=0.15.4" + "typer>=0.15.4", + "typing-extensions>=4.13.2" ] [dependency-groups] diff --git a/scripts/bump-version.py b/scripts/bump-version.py new file mode 100644 index 0000000..a0959a9 --- /dev/null +++ b/scripts/bump-version.py @@ -0,0 +1,30 @@ +"""Script responsible for bumping the version of the robust-python-demo package.""" + +import argparse + +from util import bump_version + + +def main() -> None: + """Parses args and passes through to bump_version.""" + parser: argparse.ArgumentParser = get_parser() + args: argparse.Namespace = parser.parse_args() + bump_version(increment=args.increment) + + +def get_parser() -> argparse.ArgumentParser: + """Creates the argument parser for prepare-release.""" + parser: argparse.ArgumentParser = argparse.ArgumentParser( + prog="bump-version", usage="python ./scripts/bump-version.py patch" + ) + parser.add_argument( + "increment", + type=str, + help="Increment type to use when preparing the release.", + choices=["MAJOR", "MINOR", "PATCH", "PRERELEASE"], + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/scripts/get-release-notes.py b/scripts/get-release-notes.py new file mode 100644 index 0000000..74b7271 --- /dev/null +++ b/scripts/get-release-notes.py @@ -0,0 +1,35 @@ +"""Script responsible for getting the release notes of the robust-python-demo package.""" +import argparse +from pathlib import Path + +from util import get_latest_release_notes + + +RELEASE_NOTES_PATH: Path = Path("body.md") + + +def main() -> None: + """Parses args and passes through to bump_version.""" + parser: argparse.ArgumentParser = get_parser() + args: argparse.Namespace = parser.parse_args() + release_notes: str = get_latest_release_notes() + path: Path = RELEASE_NOTES_PATH if args.path is None else args.path + path.write_text(release_notes) + + +def get_parser() -> argparse.ArgumentParser: + """Creates the argument parser for prepare-release.""" + parser: argparse.ArgumentParser = argparse.ArgumentParser( + prog="get-release-notes", usage="python ./scripts/get-release-notes.py" + ) + parser.add_argument( + "path", + type=Path, + metavar="PATH", + help="Path the changelog will be written to.", + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/scripts/prepare-release.py b/scripts/prepare-release.py deleted file mode 100644 index 1fc155f..0000000 --- a/scripts/prepare-release.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Script responsible for preparing a release of the robust-python-demo package.""" - -import argparse -import re -import shutil -import subprocess -from pathlib import Path -from re import Match -from typing import Literal -from typing import Optional -from typing import Pattern -from typing import TypeAlias - - -from util import check_dependencies -from util import remove_readonly -from util import REPO_FOLDER - - -Increment: TypeAlias = Literal["major", "minor", "patch", "prerelease"] -CZ_PATTERN: Pattern[str] = re.compile(r"bump: version (?P.*?) → (?P.*?)") - - -def main() -> None: - """Parses args and passes through to prepare_release.""" - parser: argparse.ArgumentParser = get_parser() - args: argparse.Namespace = parser.parse_args() - prepare_release(path=args.path, python_version=args.python_version) - - -def get_parser() -> argparse.ArgumentParser: - """Creates the argument parser for prepare-release.""" - parser: argparse.ArgumentParser = argparse.ArgumentParser( - prog="prepare-release", usage="python ./scripts/prepare-release.py patch" - ) - parser.add_argument( - "increment", - type=str, - help="Increment type to use when preparing the release.", - choices=["major", "minor", "patch", "prerelease"], - ) - return parser - - -def prepare_release(increment: Optional[str] = None) -> None: - """Prepares a release of the robust-python-demo package. - - Sets up a release branch from the branch develop, bumps the version, and creates a release commit. Does not tag the - release or push any changes. - """ - dry_run_cmd: list[str] = ["uvx", "cz", "bump", "--dry-run", "--yes"] - bump_cmd: list[str] = ["uvx", "cz", "bump", "--yes", "--files-only", "--changelog"] - if increment is not None: - dry_run_cmd.extend(["--increment", increment]) - bump_cmd.extend(["--increment", increment]) - - result: subprocess.CompletedProcess = subprocess.run(dry_run_cmd, cwd=REPO_FOLDER, capture_output=True) - match: Match = re.match(CZ_PATTERN, result.stdout) - current_version: str = match.group("current_version") - new_version: str = match.group("new_version") - - commands: list[list[str]] = [ - ["git", "status", "--porcelain"], - ["git", "branch", "-b", f"release/{new_version}", "develop"], - ["git", "checkout", f"release/{new_version}"], - bump_cmd, - ["git", "add", "."], - ["git", "commit", "-m", f"bump: version {current_version} → {new_version}"] - ] - - check_dependencies(path=REPO_FOLDER, dependencies=["git", "cz"]) - - for command in commands: - subprocess.run(command, cwd=REPO_FOLDER, capture_output=True, check=True) - - -if __name__ == "__main__": - main() - - diff --git a/scripts/setup-release.py b/scripts/setup-release.py new file mode 100644 index 0000000..30a5f1a --- /dev/null +++ b/scripts/setup-release.py @@ -0,0 +1,61 @@ +"""Script responsible for preparing a release of the robust-python-demo package.""" + +import argparse +import subprocess +from typing import Optional + +from util import bump_version +from util import check_dependencies +from util import create_release_branch +from util import get_package_version +from util import get_bumped_package_version +from util import REPO_FOLDER + + +def main() -> None: + """Parses args and passes through to setup_release.""" + parser: argparse.ArgumentParser = get_parser() + args: argparse.Namespace = parser.parse_args() + setup_release(increment=args.increment) + + +def get_parser() -> argparse.ArgumentParser: + """Creates the argument parser for prepare-release.""" + parser: argparse.ArgumentParser = argparse.ArgumentParser( + prog="prepare-release", usage="python ./scripts/prepare-release.py patch" + ) + parser.add_argument( + "increment", + nargs="?", + default=None, + type=str, + help="Increment type to use when preparing the release.", + choices=["MAJOR", "MINOR", "PATCH", "PRERELEASE"], + ) + return parser + + +def setup_release(increment: Optional[str] = None) -> None: + """Prepares a release of the robust-python-demo package. + + Sets up a release branch from the branch develop, bumps the version, and creates a release commit. Does not tag the + release or push any changes. + """ + check_dependencies(path=REPO_FOLDER, dependencies=["git"]) + + current_version: str = get_package_version() + new_version: str = get_bumped_package_version(increment=increment) + create_release_branch(new_version=new_version) + bump_version(increment=increment) + + commands: list[list[str]] = [ + ["git", "add", "."], + ["git", "commit", "-m", f"bump: version {current_version} → {new_version}", "--no-verify"] + ] + + for command in commands: + subprocess.run(command, cwd=REPO_FOLDER, capture_output=True, check=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/util.py b/scripts/util.py index 9161094..44dd7ae 100644 --- a/scripts/util.py +++ b/scripts/util.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Any from typing import Callable +from typing import Optional REPO_FOLDER: Path = Path(__file__).resolve().parent.parent @@ -52,3 +53,85 @@ def remove_readonly(func: Callable[[str], Any], path: str, _: Any) -> None: """ Path(path).chmod(stat.S_IWRITE) func(path) + + +def get_package_version() -> str: + """Gets the package version.""" + result: subprocess.CompletedProcess = subprocess.run( + ["uvx", "--from", "commitizen", "cz", "version", "-p"], + cwd=REPO_FOLDER, + capture_output=True + ) + return result.stdout.decode("utf-8").strip() + + +def get_bumped_package_version(increment: Optional[str] = None) -> str: + """Gets the bumped package version.""" + args: list[str] = ["uvx", "--from", "commitizen", "cz", "bump", "--get-next", "--yes", "--dry-run"] + if increment is not None: + args.extend(["--increment", increment]) + result: subprocess.CompletedProcess = subprocess.run(args, cwd=REPO_FOLDER, capture_output=True) + return result.stdout.decode("utf-8").strip() + + +def create_release_branch(new_version: str) -> None: + """Creates a release branch.""" + commands: list[list[str]] = [ + ["git", "status", "--porcelain"], + ["git", "checkout", "-b", f"release/{new_version}", "develop"], + ] + for command in commands: + subprocess.run(command, cwd=REPO_FOLDER, capture_output=True, check=True) + + +def bump_version(increment: Optional[str] = None) -> None: + """Bumps the package version.""" + bump_cmd: list[str] = ["uvx", "--from", "commitizen", "cz", "bump", "--yes", "--files-only", "--changelog"] + if increment is not None: + bump_cmd.extend(["--increment", increment]) + subprocess.run(bump_cmd, cwd=REPO_FOLDER, check=True) + + +def get_latest_tag() -> Optional[str]: + """Gets the latest git tag.""" + sort_tags: list[str] = ["git", "tag", "--sort=-creatordate"] + find_last: list[str] = ["grep", "-v", '"${GITHUB_REF_NAME}"'] + echo_none: list[str] = ["echo", "''"] + result: subprocess.CompletedProcess = subprocess.run( + [*sort_tags, "|", *find_last, "|", "tail", "-n1", "||", *echo_none], + cwd=REPO_FOLDER, + capture_output=True + ) + tag: str = result.stdout.decode("utf-8").strip() + if tag == "": + return None + return tag + + +def get_latest_release_notes() -> str: + """Gets the release notes. + + Assumes the latest_tag hasn't been applied yet. + """ + latest_tag: Optional[str] = get_latest_tag() + latest_version: str = get_package_version() + if latest_tag == latest_version: + raise ValueError( + "The latest tag and version are the same. Please ensure the release notes are taken before tagging." + ) + rev_range: str = "" if latest_tag is None else f"{latest_tag}..{latest_version}" + command: list[str] = [ + "uvx", "--from", "commitizen", "cz", "changelog", rev_range, "--dry-run", "--unreleased-version", latest_version + ] + result: subprocess.CompletedProcess = subprocess.run( + command, + cwd=REPO_FOLDER, + capture_output=True, + check=True + ) + return result.stdout.decode("utf-8") + + +def tag_release() -> None: + """Tags the release using commitizen bump with tag only.""" + subprocess.run(["uvx", "--from", "commitizen", "cz", "bump", "--tag-only", "--yes"], cwd=REPO_FOLDER, check=True) diff --git a/uv.lock b/uv.lock index 53a48bb..26a8606 100644 --- a/uv.lock +++ b/uv.lock @@ -935,18 +935,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/0e/8601d2331dea0825f3be769688b48a95f387a83c918cca6a8c9cee4b9eb7/py_serializable-2.0.0-py3-none-any.whl", hash = "sha256:1721e4c0368adeec965c183168da4b912024702f19e15e13f8577098b9a4f8fe", size = 22824, upload-time = "2025-02-09T13:41:54.236Z" }, ] -[[package]] -name = "pydocstyle" -version = "6.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "snowballstemmer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/d5385ca59fd065e3c6a5fe19f9bc9d5ea7f2509fa8c9c22fb6b2031dd953/pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1", size = 36796, upload-time = "2023-01-17T20:29:19.838Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/ea/99ddefac41971acad68f14114f38261c1f27dac0b3ec529824ebc739bdaa/pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", size = 38038, upload-time = "2023-01-17T20:29:18.094Z" }, -] - [[package]] name = "pygments" version = "2.19.1" @@ -1110,24 +1098,26 @@ dependencies = [ { name = "loguru" }, { name = "platformdirs" }, { name = "typer" }, + { name = "typing-extensions" }, ] [package.dev-dependencies] dev = [ { name = "bandit" }, { name = "commitizen" }, - { name = "furo" }, - { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "nox" }, { name = "pip-audit" }, { name = "pre-commit" }, { name = "pre-commit-hooks" }, - { name = "pydocstyle" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, +] +docs = [ + { name = "furo" }, + { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -1144,23 +1134,25 @@ requires-dist = [ { name = "loguru", specifier = ">=0.7.3" }, { name = "platformdirs", specifier = ">=4.3.8" }, { name = "typer", specifier = ">=0.15.4" }, + { name = "typing-extensions", specifier = ">=4.13.2" }, ] [package.metadata.requires-dev] dev = [ { name = "bandit", specifier = ">=1.8.3" }, { name = "commitizen", specifier = ">=4.7.0" }, - { name = "furo", specifier = ">=2024.8.6" }, - { name = "myst-parser", specifier = ">=3.0.1" }, { name = "nox", specifier = ">=2025.5.1" }, { name = "pip-audit", specifier = ">=2.9.0" }, { name = "pre-commit", specifier = ">=4.2.0" }, { name = "pre-commit-hooks", specifier = ">=5.0.0" }, - { name = "pydocstyle", specifier = ">=6.3.0" }, { name = "pyright", specifier = ">=1.1.400" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "pytest-cov", specifier = ">=6.1.1" }, { name = "ruff", specifier = ">=0.11.9" }, +] +docs = [ + { name = "furo", specifier = ">=2024.8.6" }, + { name = "myst-parser", specifier = ">=3.0.1" }, { name = "sphinx", specifier = ">=7.4.7" }, { name = "sphinx-autodoc-typehints", specifier = ">=2.3.0" }, { name = "sphinx-copybutton", specifier = ">=0.5.2" },