Repository: ryancheley/the-well-maintained-test (language: python)

Issue:
Update to include Publish releases to PyPI from GitHub Actions without a password or token
Based on the work outlined by Simon Willison [here](https://til.simonwillison.net/pypi/pypi-releases-from-github).


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
deleted file mode 100644
index f1015cc..0000000
--- a/.github/workflows/publish.yml
+++ /dev/null
@@ -1,57 +0,0 @@
-name: Publish Python Package
-
-on:
-  release:
-    types: [created, edited]
-
-jobs:
-  test:
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12-dev"]
-    steps:
-    - uses: actions/checkout@v2
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
-      with:
-        python-version: ${{ matrix.python-version }}
-    - uses: actions/cache@v2
-      name: Configure pip caching
-      with:
-        path: ~/.cache/pip
-        key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
-        restore-keys: |
-          ${{ runner.os }}-pip-
-    - name: Install dependencies
-      run: |
-        pip install -e '.[test]'
-    - name: Run tests
-      run: |
-        pytest
-  deploy:
-    runs-on: ubuntu-latest
-    needs: [test]
-    steps:
-    - uses: actions/checkout@v2
-    - name: Set up Python
-      uses: actions/setup-python@v2
-      with:
-        python-version: '3.11'
-    - uses: actions/cache@v2
-      name: Configure pip caching
-      with:
-        path: ~/.cache/pip
-        key: ${{ runner.os }}-publish-pip-${{ hashFiles('**/setup.py') }}
-        restore-keys: |
-          ${{ runner.os }}-publish-pip-
-    - name: Install dependencies
-      run: |
-        pip install setuptools wheel twine build
-    - name: Publish
-      env:
-        TWINE_USERNAME: __token__
-        TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
-      run: |
-        python -m build
-        twine upload dist/*
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..efa9d0c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,121 @@
+name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI
+
+on: push
+
+env:
+  # Change these for your project's URLs
+  PYPI_URL: https://pypi.org/project/the-well-maintained-test/
+  PYPI_TEST_URL: https://test.pypi.org/project/the-well-maintained-test/
+
+jobs:
+
+  build:
+    name: Build distribution 📦
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+    - name: Set up Python
+      uses: actions/setup-python@v5
+      with:
+        python-version: "3.x"
+    - name: Install pypa/build
+      run:
+        python3 -m pip install build --user
+    - name: Build a binary wheel and a source tarball
+      run: python3 -m build
+    - name: Store the distribution packages
+      uses: actions/upload-artifact@v4
+      with:
+        name: python-package-distributions
+        path: dist/
+
+  publish-to-pypi:
+    name: >-
+      Publish Python 🐍 distribution 📦 to PyPI
+    if: startsWith(github.ref, 'refs/tags/')  # only publish to PyPI on tag pushes
+    needs:
+    - build
+    runs-on: ubuntu-latest
+    environment:
+      name: pypi
+      url: ${{ env.PYPI_URL }}
+    permissions:
+      id-token: write  # IMPORTANT: mandatory for trusted publishing
+    steps:
+    - name: Download all the dists
+      uses: actions/download-artifact@v4
+      with:
+        name: python-package-distributions
+        path: dist/
+    - name: Publish distribution 📦 to PyPI
+      uses: pypa/gh-action-pypi-publish@release/v1.10
+
+  github-release:
+    name: >-
+      Sign the Python 🐍 distribution 📦 with Sigstore
+      and upload them to GitHub Release
+    needs:
+    - publish-to-pypi
+    runs-on: ubuntu-latest
+
+    permissions:
+      contents: write  # IMPORTANT: mandatory for making GitHub Releases
+      id-token: write  # IMPORTANT: mandatory for sigstore
+
+    steps:
+    - name: Download all the dists
+      uses: actions/download-artifact@v4
+      with:
+        name: python-package-distributions
+        path: dist/
+    - name: Sign the dists with Sigstore
+      uses: sigstore/gh-action-sigstore-python@v3.0.0
+      with:
+        inputs: >-
+          ./dist/*.tar.gz
+          ./dist/*.whl
+    - name: Create GitHub Release
+      env:
+        GITHUB_TOKEN: ${{ github.token }}
+      run: >-
+        gh release create
+        '${{ github.ref_name }}'
+        --repo '${{ github.repository }}'
+        --notes ""
+    - name: Upload artifact signatures to GitHub Release
+      env:
+        GITHUB_TOKEN: ${{ github.token }}
+      # Upload to GitHub Release using the `gh` CLI.
+      # `dist/` contains the built packages, and the
+      # sigstore-produced signatures and certificates.
+      run: >-
+        gh release upload
+        '${{ github.ref_name }}' dist/**
+        --repo '${{ github.repository }}'
+
+  # publish-to-testpypi:
+  #   name: Publish Python 🐍 distribution 📦 to TestPyPI
+  #   if: startsWith(github.ref, 'refs/tags/')  # only publish to TestPyPI on tag pushes
+  #   needs:
+  #   - build
+  #   runs-on: ubuntu-latest
+
+  #   environment:
+  #     name: testpypi
+  #     url: ${{ env.PYPI_TEST_URL }}
+
+  #   permissions:
+  #     id-token: write  # IMPORTANT: mandatory for trusted publishing
+
+  #   steps:
+  #   - name: Download all the dists
+  #     uses: actions/download-artifact@v4
+  #     with:
+  #       name: python-package-distributions
+  #       path: dist/
+  #   - name: Publish distribution 📦 to TestPyPI
+  #     uses: pypa/gh-action-pypi-publish@release/v1.10
+  #     with:
+  #       repository-url: https://test.pypi.org/legacy/
+  #       skip-existing: true
diff --git a/noxfile.py b/noxfile.py
new file mode 100644
index 0000000..b154415
--- /dev/null
+++ b/noxfile.py
@@ -0,0 +1,23 @@
+import nox
+
+PYTHON_VERSIONS = [
+    "3.8",
+    "3.9",
+    "3.10",
+    "3.11",
+    "3.12",
+    "3.13",
+]
+
+
+@nox.session(python=PYTHON_VERSIONS)
+def tests(session):
+    # Install pytest and any other test dependencies
+    session.install("pytest")
+    session.install("pytest-cov")
+
+    # Install the package itself
+    session.install(".")
+
+    # Run pytest with coverage reporting
+    session.run("pytest", "--cov", "--cov-report=term-missing", *session.posargs)


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 6489908..f884ce0 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -15,7 +15,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12-dev"]
+        python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"]
     steps:
     - uses: actions/checkout@v2
     - name: Set up Python ${{ matrix.python-version }}
diff --git a/src/the_well_maintained_test/utils.py b/src/the_well_maintained_test/utils.py
index 384f468..923ab23 100644
--- a/src/the_well_maintained_test/utils.py
+++ b/src/the_well_maintained_test/utils.py
@@ -1,6 +1,6 @@
 import json
 import re
-from datetime import datetime
+from datetime import datetime, timezone
 from gettext import ngettext
 from operator import attrgetter
 from pathlib import Path
@@ -74,7 +74,12 @@ def bug_responding(bugs_url: str, headers: dict) -> str:
         bug_comment_list = sorted(bug_comment_list, key=attrgetter("create_date"), reverse=True)
         if bug_comment_list:
             bug_turn_around_time_reply_days = (bug_comment_list[0].create_date - bug_create_date).days
-            days_since_last_bug_comment = (datetime.utcnow() - bug_comment_list[0].create_date).days
+            # If bug_comment_list[0].create_date is naive, make it timezone-aware
+            if bug_comment_list[0].create_date.tzinfo is None:
+                create_date = bug_comment_list[0].create_date.replace(tzinfo=timezone.utc)
+            else:
+                create_date = bug_comment_list[0].create_date
+            days_since_last_bug_comment = (datetime.now(timezone.utc) - create_date).days
             # TODO: add logic to better colorize the message
             message1 = f"The maintainer took {bug_turn_around_time_reply_days} "
             message1 += "days to respond to the bug report"
@@ -197,8 +202,8 @@ def commit_in_last_year(commits_url: str, headers: dict) -> str:
     """
     r = requests.get(commits_url, headers=headers).json()
     last_commit_date = r.get("commit").get("author").get("date")
-    last_commit_date = datetime.strptime(last_commit_date, "%Y-%m-%dT%H:%M:%SZ")
-    days_since_last_commit = (datetime.utcnow() - last_commit_date).days
+    last_commit_date = datetime.strptime(last_commit_date, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
+    days_since_last_commit = (datetime.now(timezone.utc) - last_commit_date).days
     if days_since_last_commit > 365:
         message = f"[red]No. The last commit was {days_since_last_commit} days ago"
     else:
@@ -216,8 +221,8 @@ def release_in_last_year(pypi_api_url: str) -> str:
     releases = _get_release_date(r)
     last_release_date = releases[0].upload_time
     version = releases[0].version
-    last_release_date = datetime.strptime(last_release_date, "%Y-%m-%dT%H:%M:%S")
-    days_since_last_release = (datetime.utcnow() - last_release_date).days
+    last_release_date = datetime.strptime(last_release_date, "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc)
+    days_since_last_release = (datetime.now(timezone.utc) - last_release_date).days
     if days_since_last_release > 365:
         message = f"[red]No. Version {version} was last released {days_since_last_release} days ago"
     else: