diff --git a/.github/workflows/release-to-pypi.yaml b/.github/workflows/release-to-pypi.yaml new file mode 100644 index 0000000..915ae0e --- /dev/null +++ b/.github/workflows/release-to-pypi.yaml @@ -0,0 +1,62 @@ +--- +name: Release to PyPI + +on: + pull_request: + push: + tags: + - v* + +jobs: + build: + name: Build dist package + runs-on: ubuntu-22.04 + timeout-minutes: 5 + + steps: + - name: Clone the code + uses: actions/checkout@v4 + + - uses: hynek/build-and-inspect-python-package@v2 + + verify: + name: Verify versions + runs-on: ubuntu-22.04 + timeout-minutes: 5 + if: github.repository_owner == 'kraken-tech' && github.ref == 'refs/heads/main' + + steps: + - name: Clone the code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install requirements + run: make install + + - name: Verify version + run: ./scripts/verify-version-tag.py + + release: + name: Publish to pypi.org + environment: release + if: github.repository_owner == 'kraken-tech' && github.ref == 'refs/heads/main' + needs: [build, verify] + runs-on: ubuntu-22.04 + timeout-minutes: 5 + + permissions: + id-token: write + + steps: + - name: Download packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + + - name: Upload package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/pyproject.toml b/pyproject.toml index bbf3c6d..d6179b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ where = ["src"] name = "django_integrity" version = "0.0-alpha" license.file = "LICENSE" +readme = "README.md" requires-python = ">=3.10" dependencies = [ # We cannot decide for users if they want to use psycopg2, psycopg2-binary, or @@ -64,6 +65,7 @@ dev = [ # CLI utils "rich", + "tomli >= 1.1.0 ; python_version < '3.11'", "typer", ] diff --git a/requirements/development.txt b/requirements/development.txt index 0297f72..8801407 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -1,12 +1,14 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.10 # by the following command: # # pip-compile --extra=dev --output-file=requirements/development.txt --unsafe-package=django pyproject.toml # -asgiref==3.7.2 - # via django -cachetools==5.3.2 +asgiref==3.8.1 + # via + # django + # django-stubs +cachetools==5.3.3 # via tox chardet==5.2.0 # via tox @@ -18,13 +20,15 @@ distlib==0.3.8 # via virtualenv dj-database-url==2.1.0 # via django_integrity (pyproject.toml) -django-stubs==4.2.7 +django-stubs==5.0.0 # via django_integrity (pyproject.toml) -django-stubs-ext==4.2.7 +django-stubs-ext==5.0.0 # via django-stubs -environs==10.3.0 +environs==11.0.0 # via django_integrity (pyproject.toml) -filelock==3.13.1 +exceptiongroup==1.2.1 + # via pytest +filelock==3.14.0 # via # tox # virtualenv @@ -32,35 +36,35 @@ iniconfig==2.0.0 # via pytest markdown-it-py==3.0.0 # via rich -marshmallow==3.20.2 +marshmallow==3.21.2 # via environs mdurl==0.1.2 # via markdown-it-py -mypy==1.8.0 +mypy==1.10.0 # via django_integrity (pyproject.toml) mypy-extensions==1.0.0 # via mypy -mypy-json-report==1.1.0 +mypy-json-report==1.2.0 # via django_integrity (pyproject.toml) -packaging==23.2 +packaging==24.0 # via # marshmallow # pyproject-api # pytest # tox -platformdirs==4.2.0 +platformdirs==4.2.1 # via # tox # virtualenv -pluggy==1.4.0 +pluggy==1.5.0 # via # pytest # tox -pygments==2.17.2 +pygments==2.18.0 # via rich pyproject-api==1.6.1 # via tox -pytest==8.0.0 +pytest==8.2.0 # via # django_integrity (pyproject.toml) # pytest-django @@ -68,28 +72,39 @@ pytest-django==4.8.0 # via django_integrity (pyproject.toml) python-dotenv==1.0.1 # via environs -rich==13.7.0 - # via django_integrity (pyproject.toml) -ruff==0.2.1 +rich==13.7.1 + # via + # django_integrity (pyproject.toml) + # typer +ruff==0.4.3 # via django_integrity (pyproject.toml) -sqlparse==0.4.4 +shellingham==1.5.4 + # via typer +sqlparse==0.5.0 # via django -tox==4.12.1 +tomli==2.0.1 ; python_version < "3.11" + # via + # django-stubs + # django_integrity (pyproject.toml) + # mypy + # pyproject-api + # pytest + # tox +tox==4.15.0 # via django_integrity (pyproject.toml) -typer==0.9.0 +typer==0.12.3 # via django_integrity (pyproject.toml) -types-pytz==2024.1.0.20240203 - # via django-stubs -types-pyyaml==6.0.12.12 +types-pyyaml==6.0.12.20240311 # via django-stubs -typing-extensions==4.9.0 +typing-extensions==4.11.0 # via + # asgiref # dj-database-url # django-stubs # django-stubs-ext # mypy # typer -virtualenv==20.25.0 +virtualenv==20.26.1 # via tox # The following packages are considered to be unsafe in a requirements file: diff --git a/scripts/verify-version-tag.py b/scripts/verify-version-tag.py new file mode 100755 index 0000000..b593cb4 --- /dev/null +++ b/scripts/verify-version-tag.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +""" +A script to help with making a new release. + +This script verifies that the current commit has a tag, +that the tag matches the version in the `pyproject.toml` file, +and that the tag is in the CHANGELOG. +""" +import pathlib +import re +import subprocess +import sys + +import rich +import typer +from packaging.version import InvalidVersion, Version + + +if sys.version_info >= (3, 11): + import tomllib +else: + # We can remove this when we drop support for Python 3.10. + import tomli as tomllib + + +PYPROJECT_FILE = pathlib.Path(__file__).resolve().parent.parent / "pyproject.toml" +CHANGELOG_FILE = pathlib.Path(__file__).resolve().parent.parent / "CHANGELOG.md" +app = typer.Typer() + + +@app.command() +def main(): + # Get the tags on the current commit. + tags = ( + subprocess.check_output(["git", "tag", "--points-at", "HEAD"]).decode().split() + ) + + # Find a tag that looks like a version. + versions: list[Version] = [] + for tag in tags: + try: + version = Version(tag) + except InvalidVersion: + rich.print(f"[yellow]Skipping non-version tag:[/] {tag}") + else: + versions.append(version) + + if not versions: + rich.print("[red]No version tags found.") + raise typer.Abort() + elif len(versions) > 1: + rich.print(f"[red]Multiple version tags found:[/] {versions}.") + raise typer.Abort() + + (tag_version,) = versions + + # Get the version from the pyproject.toml file. + pyproject_content = PYPROJECT_FILE.read_text() + project_config = tomllib.loads(pyproject_content) + project_version_str = project_config["project"]["version"] + project_version = Version(project_version_str) + + # Check that the tag matches the version. + if tag_version == project_version: + rich.print("[green]Tag matches version in pyproject.toml.") + else: + rich.print("[red]Versions do not match:") + rich.print(f" Git tag: {tag_version}") + rich.print(f" Package: {project_version}") + raise typer.Abort() + + # Check that the tag is in the CHANGELOG. + return_code = subprocess.run( + ["git", "grep", rf"\bv{tag_version}\b", "HEAD", "--", str(CHANGELOG_FILE)], + ).returncode + if return_code == 0: + rich.print("[green]Tag found in CHANGELOG.") + else: + rich.print(f"[red]Tag not committed to CHANGELOG:[/] {tag_version}.") + raise typer.Abort() + + +if __name__ == "__main__": + app()