mirror of
https://github.com/django-components/django-components.git
synced 2025-10-12 06:51:58 +00:00
ci: check supported versions once a week (#1419)
Some checks are pending
Docs - build & deploy / docs (push) Waiting to run
Run tests / build (ubuntu-latest, 3.10) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.11) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.12) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.8) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.9) (push) Waiting to run
Run tests / build (windows-latest, 3.10) (push) Waiting to run
Run tests / build (windows-latest, 3.11) (push) Waiting to run
Run tests / build (windows-latest, 3.12) (push) Waiting to run
Run tests / build (windows-latest, 3.13) (push) Waiting to run
Run tests / build (windows-latest, 3.8) (push) Waiting to run
Run tests / build (windows-latest, 3.9) (push) Waiting to run
Run tests / test_docs (3.13) (push) Waiting to run
Run tests / test_sampleproject (3.13) (push) Waiting to run
Some checks are pending
Docs - build & deploy / docs (push) Waiting to run
Run tests / build (ubuntu-latest, 3.10) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.11) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.12) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.8) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.9) (push) Waiting to run
Run tests / build (windows-latest, 3.10) (push) Waiting to run
Run tests / build (windows-latest, 3.11) (push) Waiting to run
Run tests / build (windows-latest, 3.12) (push) Waiting to run
Run tests / build (windows-latest, 3.13) (push) Waiting to run
Run tests / build (windows-latest, 3.8) (push) Waiting to run
Run tests / build (windows-latest, 3.9) (push) Waiting to run
Run tests / test_docs (3.13) (push) Waiting to run
Run tests / test_sampleproject (3.13) (push) Waiting to run
This commit is contained in:
parent
91012829ff
commit
8a979cd821
7 changed files with 558 additions and 33 deletions
36
.github/workflows/maint-supported-versions.yml
vendored
Normal file
36
.github/workflows/maint-supported-versions.yml
vendored
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
name: Check supported versions
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run every Sunday at 2:00 AM UTC
|
||||||
|
- cron: '0 2 * * 0'
|
||||||
|
workflow_dispatch: # Allow manual triggering
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-versions:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
cache: "pip"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install -r requirements-dev.txt
|
||||||
|
|
||||||
|
- name: Check supported versions
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
python scripts/supported_versions.py check
|
|
@ -1,10 +1,10 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: local
|
||||||
# Ruff version.
|
|
||||||
rev: v0.12.9
|
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# See https://stackoverflow.com/questions/70778806
|
||||||
- id: ruff-check
|
- id: tox
|
||||||
args: [ --fix ]
|
name: tox
|
||||||
# Run the formatter.
|
entry: .venv/bin/tox
|
||||||
- id: ruff-format
|
args: ["-e", "mypy,ruff"]
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
|
|
@ -310,14 +310,21 @@ Head over to [Dev guides](./devguides/dependency_mgmt.md) for a deep dive into h
|
||||||
|
|
||||||
### Updating supported versions
|
### Updating supported versions
|
||||||
|
|
||||||
The `scripts/supported_versions.py` script can be used to update the supported versions.
|
The `scripts/supported_versions.py` script manages the supported Python and Django versions for the project.
|
||||||
|
|
||||||
|
The script runs automatically via GitHub Actions once a week to check for version updates. If changes are detected, it creates a GitHub issue with the necessary updates. See the [`maint-supported-versions.yml`](https://github.com/django-components/django-components/blob/master/.github/workflows/maint-supported-versions.yml) workflow.
|
||||||
|
|
||||||
|
You can also run the script manually:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python scripts/supported_versions.py
|
# Check if versions need updating
|
||||||
|
python scripts/supported_versions.py check
|
||||||
|
|
||||||
|
# Generate configuration snippets for manual updates
|
||||||
|
python scripts/supported_versions.py generate
|
||||||
```
|
```
|
||||||
|
|
||||||
This will check the current versions of Django and Python, and will print to the terminal
|
The `generate` command will print to the terminal all the places that need updating and what to set them to.
|
||||||
all the places that need updating and what to set them to.
|
|
||||||
|
|
||||||
### Updating link references
|
### Updating link references
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,73 @@
|
||||||
# ruff: noqa: T201, S310
|
# ruff: noqa: BLE001, PLW2901, RUF001, S310, T201
|
||||||
|
"""
|
||||||
|
This script manages the info about supported Python and Django versions.
|
||||||
|
|
||||||
|
The script fetches the latest supported version information from official sources:
|
||||||
|
- Python versions from https://devguide.python.org/versions/
|
||||||
|
- Django versions and compatibility matrix from https://docs.djangoproject.com/
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
generate: Generates instructions for updating various files (tox.ini, pyproject.toml,
|
||||||
|
GitHub Actions, documentation) based on current supported versions
|
||||||
|
|
||||||
|
check: Compares the current compatibility table in `docs/overview/compatibility.md`
|
||||||
|
with the latest official version information. If differences are found,
|
||||||
|
creates a GitHub issue to track the needed updates.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/supported_versions.py generate
|
||||||
|
python scripts/supported_versions.py check
|
||||||
|
|
||||||
|
# For GitHub issue creation (check command):
|
||||||
|
GITHUB_TOKEN=your_token python scripts/supported_versions.py check
|
||||||
|
|
||||||
|
Files updated by this script:
|
||||||
|
- docs/overview/compatibility.md (compatibility table)
|
||||||
|
- tox.ini (test environments)
|
||||||
|
- pyproject.toml (Python classifiers)
|
||||||
|
- .github/workflows/tests.yml (CI matrix)
|
||||||
|
- docs/community/development.md (development setup)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any, Callable, Dict, List, Tuple
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable, Dict, List, NamedTuple, Tuple
|
||||||
from urllib import request
|
from urllib import request
|
||||||
|
|
||||||
Version = Tuple[int, ...]
|
Version = Tuple[int, ...]
|
||||||
VersionMapping = Dict[Version, List[Version]]
|
VersionMapping = Dict[Version, List[Version]]
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoVersionChanges(NamedTuple):
|
||||||
|
added: List[Version]
|
||||||
|
removed: List[Version]
|
||||||
|
|
||||||
|
|
||||||
|
class VersionDifferences(NamedTuple):
|
||||||
|
added_python_versions: List[Version]
|
||||||
|
removed_python_versions: List[Version]
|
||||||
|
changed_django_versions: Dict[Version, DjangoVersionChanges]
|
||||||
|
has_changes: bool
|
||||||
|
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# GET DATA FROM OFFICIAL SOURCES
|
||||||
|
######################################
|
||||||
|
|
||||||
|
|
||||||
|
HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; django-components version script)"}
|
||||||
|
|
||||||
|
|
||||||
|
def filter_dict(d: Dict, filter_fn: Callable[[Any], bool]) -> Dict:
|
||||||
|
return dict(filter(filter_fn, d.items()))
|
||||||
|
|
||||||
|
|
||||||
def cut_by_content(content: str, cut_from: str, cut_to: str) -> str:
|
def cut_by_content(content: str, cut_from: str, cut_to: str) -> str:
|
||||||
return content.split(cut_from)[1].split(cut_to)[0]
|
return content.split(cut_from)[1].split(cut_to)[0]
|
||||||
|
|
||||||
|
@ -18,7 +77,8 @@ def keys_from_content(content: str) -> List[str]:
|
||||||
|
|
||||||
|
|
||||||
def get_python_supported_version(url: str) -> List[Version]:
|
def get_python_supported_version(url: str) -> List[Version]:
|
||||||
with request.urlopen(url) as response:
|
req = request.Request(url, headers=HEADERS)
|
||||||
|
with request.urlopen(req) as response:
|
||||||
response_content = response.read()
|
response_content = response.read()
|
||||||
|
|
||||||
content = response_content.decode("utf-8")
|
content = response_content.decode("utf-8")
|
||||||
|
@ -39,7 +99,8 @@ def get_python_supported_version(url: str) -> List[Version]:
|
||||||
|
|
||||||
|
|
||||||
def get_django_to_python_versions(url: str) -> VersionMapping:
|
def get_django_to_python_versions(url: str) -> VersionMapping:
|
||||||
with request.urlopen(url) as response:
|
req = request.Request(url, headers=HEADERS)
|
||||||
|
with request.urlopen(req) as response:
|
||||||
response_content = response.read()
|
response_content = response.read()
|
||||||
|
|
||||||
content = response_content.decode("utf-8")
|
content = response_content.decode("utf-8")
|
||||||
|
@ -69,7 +130,8 @@ def get_django_to_python_versions(url: str) -> VersionMapping:
|
||||||
|
|
||||||
def get_django_supported_versions(url: str) -> List[Tuple[int, ...]]:
|
def get_django_supported_versions(url: str) -> List[Tuple[int, ...]]:
|
||||||
"""Extract Django versions from the HTML content, e.g. `5.0` or `4.2`"""
|
"""Extract Django versions from the HTML content, e.g. `5.0` or `4.2`"""
|
||||||
with request.urlopen(url) as response:
|
req = request.Request(url, headers=HEADERS)
|
||||||
|
with request.urlopen(req) as response:
|
||||||
response_content = response.read()
|
response_content = response.read()
|
||||||
|
|
||||||
content = response_content.decode("utf-8")
|
content = response_content.decode("utf-8")
|
||||||
|
@ -94,7 +156,8 @@ def get_django_supported_versions(url: str) -> List[Tuple[int, ...]]:
|
||||||
|
|
||||||
|
|
||||||
def get_latest_version(url: str) -> Version:
|
def get_latest_version(url: str) -> Version:
|
||||||
with request.urlopen(url) as response:
|
req = request.Request(url, headers=HEADERS)
|
||||||
|
with request.urlopen(req) as response:
|
||||||
response_content = response.read()
|
response_content = response.read()
|
||||||
|
|
||||||
content = response_content.decode("utf-8")
|
content = response_content.decode("utf-8")
|
||||||
|
@ -117,6 +180,28 @@ def build_python_to_django(django_to_python: VersionMapping, latest_version: Ver
|
||||||
return python_to_django
|
return python_to_django
|
||||||
|
|
||||||
|
|
||||||
|
def get_python_to_django() -> VersionMapping:
|
||||||
|
"""Get the Python to Django version mapping as extracted from the websites."""
|
||||||
|
django_to_python = get_django_to_python_versions("https://docs.djangoproject.com/en/dev/faq/install/")
|
||||||
|
django_supported_versions = get_django_supported_versions("https://www.djangoproject.com/download/")
|
||||||
|
latest_version = get_latest_version("https://www.djangoproject.com/download/")
|
||||||
|
|
||||||
|
supported_django_to_python = filter_dict(django_to_python, lambda item: item[0] in django_supported_versions)
|
||||||
|
python_to_django = build_python_to_django(supported_django_to_python, latest_version)
|
||||||
|
# NOTE: Uncomment the below if you want to include only those Python versions
|
||||||
|
# that are still actively supported. Otherwise, we include all Python versions
|
||||||
|
# that are compatible with supported Django versions.
|
||||||
|
# active_python = get_python_supported_version("https://devguide.python.org/versions/")
|
||||||
|
# python_to_django = filter_dict(python_to_django, lambda item: item[0] in active_python)
|
||||||
|
|
||||||
|
return python_to_django
|
||||||
|
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# GENERATE COMMAND
|
||||||
|
######################################
|
||||||
|
|
||||||
|
|
||||||
def env_format(version_tuple: Version, divider: str = "") -> str:
|
def env_format(version_tuple: Version, divider: str = "") -> str:
|
||||||
return divider.join(str(num) for num in version_tuple)
|
return divider.join(str(num) for num in version_tuple)
|
||||||
|
|
||||||
|
@ -222,26 +307,15 @@ def build_pyenv(python_to_django: VersionMapping) -> str:
|
||||||
def build_ci_python_versions(python_to_django: VersionMapping) -> str:
|
def build_ci_python_versions(python_to_django: VersionMapping) -> str:
|
||||||
# Outputs python-version, like: ['3.8', '3.9', '3.10', '3.11', '3.12']
|
# Outputs python-version, like: ['3.8', '3.9', '3.10', '3.11', '3.12']
|
||||||
lines = [
|
lines = [
|
||||||
f"'{env_format(python_version, divider='.')}'" for python_version, django_versions in python_to_django.items()
|
f"'{env_format(python_version, divider='.')}'" for python_version, _django_versions in python_to_django.items()
|
||||||
]
|
]
|
||||||
lines_formatted = " " * 8 + f"python-version: [{', '.join(lines)}]"
|
lines_formatted = " " * 8 + f"python-version: [{', '.join(lines)}]"
|
||||||
return lines_formatted
|
return lines_formatted
|
||||||
|
|
||||||
|
|
||||||
def filter_dict(d: Dict, filter_fn: Callable[[Any], bool]) -> Dict:
|
def command_generate() -> None:
|
||||||
return dict(filter(filter_fn, d.items()))
|
print("🔄 Fetching latest version information...")
|
||||||
|
python_to_django = get_python_to_django()
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
active_python = get_python_supported_version("https://devguide.python.org/versions/")
|
|
||||||
django_to_python = get_django_to_python_versions("https://docs.djangoproject.com/en/dev/faq/install/")
|
|
||||||
django_supported_versions = get_django_supported_versions("https://www.djangoproject.com/download/")
|
|
||||||
latest_version = get_latest_version("https://www.djangoproject.com/download/")
|
|
||||||
|
|
||||||
supported_django_to_python = filter_dict(django_to_python, lambda item: item[0] in django_supported_versions)
|
|
||||||
python_to_django = build_python_to_django(supported_django_to_python, latest_version)
|
|
||||||
|
|
||||||
python_to_django = filter_dict(python_to_django, lambda item: item[0] in active_python)
|
|
||||||
|
|
||||||
tox_envlist = build_tox_envlist(python_to_django)
|
tox_envlist = build_tox_envlist(python_to_django)
|
||||||
print("Add this to tox.ini:\n")
|
print("Add this to tox.ini:\n")
|
||||||
|
@ -285,5 +359,413 @@ def main() -> None:
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# CHECK COMMAND
|
||||||
|
######################################
|
||||||
|
|
||||||
|
|
||||||
|
def parse_compatibility_markdown(file_path: Path) -> VersionMapping:
|
||||||
|
"""
|
||||||
|
Extract compatibility table from markdown file with following format:
|
||||||
|
|
||||||
|
```
|
||||||
|
| Python version | Django version |
|
||||||
|
|----------------|----------------|
|
||||||
|
| 3.9 | 4.2 |
|
||||||
|
| 3.10 | 4.2, 5.1, 5.2 |
|
||||||
|
| 3.11 | 4.2, 5.1, 5.2 |
|
||||||
|
| 3.12 | 4.2, 5.1, 5.2 |
|
||||||
|
| 3.13 | 5.1, 5.2 |
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with file_path.open(encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Error: Could not find compatibility file at {file_path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Find the table section
|
||||||
|
lines = content.split("\n")
|
||||||
|
table_start = -1
|
||||||
|
table_end = -1
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
# Search for the table headers line
|
||||||
|
# `| Python version | Django version |`
|
||||||
|
if re.search(r"\|\s*Python\s+version\s*\|\s*Django\s+version\s*\|", line, re.IGNORECASE):
|
||||||
|
table_start = i + 2 # Skip header and separator line
|
||||||
|
# Search for the end of the table
|
||||||
|
elif table_start != -1 and (line.strip() == "" or not line.startswith("|")):
|
||||||
|
table_end = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if table_start == -1:
|
||||||
|
print("Error: Could not find compatibility table in markdown file")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if table_end == -1:
|
||||||
|
# If the end of the table is not found, use the last line of the file
|
||||||
|
table_end = len(lines)
|
||||||
|
|
||||||
|
# Parse table rows
|
||||||
|
# `| 3.10 | 4.2, 5.1, 5.2 |`
|
||||||
|
python_to_django: VersionMapping = {}
|
||||||
|
for i in range(table_start, table_end):
|
||||||
|
# Skip empty and non-table lines
|
||||||
|
line = lines[i].strip()
|
||||||
|
if not line or not line.startswith("|"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Split by | and clean up
|
||||||
|
parts = [part.strip() for part in line.split("|")[1:-1]] # Remove empty first/last
|
||||||
|
if len(parts) != 2:
|
||||||
|
raise ValueError(f"Unexpected table row: {line}")
|
||||||
|
|
||||||
|
python_version_str = parts[0].strip()
|
||||||
|
django_versions_str = parts[1].strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
python_version = version_to_tuple(python_version_str)
|
||||||
|
django_versions = []
|
||||||
|
for version_str in django_versions_str.split(","):
|
||||||
|
version_str = version_str.strip()
|
||||||
|
if version_str:
|
||||||
|
django_versions.append(version_to_tuple(version_str))
|
||||||
|
|
||||||
|
if django_versions:
|
||||||
|
python_to_django[python_version] = django_versions
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(f"Invalid version string in table row '{line}': {e}") from e
|
||||||
|
|
||||||
|
return python_to_django
|
||||||
|
|
||||||
|
|
||||||
|
def compare_version_mappings(current: VersionMapping, expected: VersionMapping) -> VersionDifferences:
|
||||||
|
current_pythons = set(current.keys())
|
||||||
|
expected_pythons = set(expected.keys())
|
||||||
|
|
||||||
|
# Find added/removed Python versions
|
||||||
|
added_pythons = expected_pythons - current_pythons
|
||||||
|
removed_pythons = current_pythons - expected_pythons
|
||||||
|
|
||||||
|
added_python_versions = sorted(added_pythons)
|
||||||
|
removed_python_versions = sorted(removed_pythons)
|
||||||
|
|
||||||
|
# Find changed Django versions for existing Python versions
|
||||||
|
changed_django_versions = {}
|
||||||
|
common_pythons = current_pythons & expected_pythons
|
||||||
|
for python_version in common_pythons:
|
||||||
|
current_djangos = set(current[python_version])
|
||||||
|
expected_djangos = set(expected[python_version])
|
||||||
|
|
||||||
|
if current_djangos != expected_djangos:
|
||||||
|
changed_django_versions[python_version] = DjangoVersionChanges(
|
||||||
|
added=sorted(expected_djangos - current_djangos),
|
||||||
|
removed=sorted(current_djangos - expected_djangos),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if there are any changes
|
||||||
|
has_changes = bool(added_pythons) or bool(removed_pythons) or bool(changed_django_versions)
|
||||||
|
|
||||||
|
return VersionDifferences(
|
||||||
|
added_python_versions=added_python_versions,
|
||||||
|
removed_python_versions=removed_python_versions,
|
||||||
|
changed_django_versions=changed_django_versions,
|
||||||
|
has_changes=has_changes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def command_check(repo_owner: str = "django-components", repo_name: str = "django-components") -> None:
|
||||||
|
"""Check if supported versions need updating and create GitHub issue if needed"""
|
||||||
|
print("🔄 Checking supported versions...")
|
||||||
|
|
||||||
|
# Get current versions from markdown
|
||||||
|
compatibility_file = Path("docs/overview/compatibility.md")
|
||||||
|
try:
|
||||||
|
current_python_to_django = parse_compatibility_markdown(compatibility_file)
|
||||||
|
print(f"📖 Parsed current versions from {compatibility_file}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error parsing compatibility file: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Get expected versions from official sources
|
||||||
|
try:
|
||||||
|
expected_python_to_django = get_python_to_django()
|
||||||
|
print("🌐 Fetched expected versions from official sources")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error fetching expected versions: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Compare versions
|
||||||
|
differences = compare_version_mappings(current_python_to_django, expected_python_to_django)
|
||||||
|
|
||||||
|
if not differences.has_changes:
|
||||||
|
print("✅ Supported versions are up to date!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("⚠️ Supported versions need updating!")
|
||||||
|
|
||||||
|
# Print differences
|
||||||
|
if differences.added_python_versions:
|
||||||
|
print("➕ Added Python versions:")
|
||||||
|
for version in differences.added_python_versions:
|
||||||
|
print(f" - Python {env_format(version, divider='.')}")
|
||||||
|
|
||||||
|
if differences.removed_python_versions:
|
||||||
|
print("➖ Removed Python versions:")
|
||||||
|
for version in differences.removed_python_versions:
|
||||||
|
print(f" - Python {env_format(version, divider='.')}")
|
||||||
|
|
||||||
|
if differences.changed_django_versions:
|
||||||
|
print("🔄 Changed Django version support:")
|
||||||
|
for python_version, changes in differences.changed_django_versions.items():
|
||||||
|
python_str = env_format(python_version, divider=".")
|
||||||
|
print(f" Python {python_str}:")
|
||||||
|
for django_version in changes.added:
|
||||||
|
django_str = env_format(django_version, divider=".")
|
||||||
|
print(f" ✅ Added Django {django_str}")
|
||||||
|
for django_version in changes.removed:
|
||||||
|
django_str = env_format(django_version, divider=".")
|
||||||
|
print(f" ❌ Removed Django {django_str}")
|
||||||
|
|
||||||
|
# Create GitHub issue
|
||||||
|
github_token = os.environ.get("GITHUB_TOKEN")
|
||||||
|
|
||||||
|
if not github_token:
|
||||||
|
print("\n⚠️ GITHUB_TOKEN environment variable not set.")
|
||||||
|
print("Set GITHUB_TOKEN to create GitHub issues automatically.")
|
||||||
|
print("Run `python scripts/supported_versions.py generate` to get updated configurations.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Generate issue title and body
|
||||||
|
title = create_github_issue_title(differences)
|
||||||
|
body = generate_issue_body(differences, current_python_to_django, expected_python_to_django)
|
||||||
|
|
||||||
|
# Check for existing issues
|
||||||
|
print("🔍 Checking for existing issues...")
|
||||||
|
if check_existing_github_issue(title, repo_owner, repo_name, github_token):
|
||||||
|
print("ℹ️ Similar issue already exists. Skipping issue creation.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create the issue
|
||||||
|
print(f"📝 Creating GitHub issue: {title}")
|
||||||
|
success = create_github_issue(title, body, repo_owner, repo_name, github_token)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
print("❌ Failed to create GitHub issue")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# CHECK COMMAND - GITHUB ISSUE
|
||||||
|
######################################
|
||||||
|
|
||||||
|
|
||||||
|
def create_github_issue_title(differences: VersionDifferences) -> str:
|
||||||
|
"""
|
||||||
|
Generate a GitHub issue title based on version differences
|
||||||
|
|
||||||
|
We rely on this title to find the issue in the future to avoid duplicates.
|
||||||
|
"""
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
if differences.added_python_versions:
|
||||||
|
for version in differences.added_python_versions:
|
||||||
|
version_str = env_format(version, divider=".")
|
||||||
|
parts.append(f"Add Python {version_str}")
|
||||||
|
|
||||||
|
if differences.removed_python_versions:
|
||||||
|
for version in differences.removed_python_versions:
|
||||||
|
version_str = env_format(version, divider=".")
|
||||||
|
parts.append(f"Remove Python {version_str}")
|
||||||
|
|
||||||
|
# Check for Django version changes
|
||||||
|
for python_version, changes in differences.changed_django_versions.items():
|
||||||
|
python_str = env_format(python_version, divider=".")
|
||||||
|
if changes.added:
|
||||||
|
for django_version in changes.added:
|
||||||
|
django_str = env_format(django_version, divider=".")
|
||||||
|
parts.append(f"Add Django {django_str} for Python {python_str}")
|
||||||
|
if changes.removed:
|
||||||
|
for django_version in changes.removed:
|
||||||
|
django_str = env_format(django_version, divider=".")
|
||||||
|
parts.append(f"Remove Django {django_str} for Python {python_str}")
|
||||||
|
|
||||||
|
if not parts:
|
||||||
|
return "[maint] Update supported versions"
|
||||||
|
|
||||||
|
# Create a concise title
|
||||||
|
if len(parts) == 1:
|
||||||
|
return f"[maint] {parts[0]} to supported versions"
|
||||||
|
return f"[maint] Update supported versions ({len(parts)} changes)"
|
||||||
|
|
||||||
|
|
||||||
|
def check_existing_github_issue(title: str, repo_owner: str, repo_name: str, token: str) -> bool:
|
||||||
|
"""Check if a GitHub issue with similar title already exists"""
|
||||||
|
# Search for issues with similar titles
|
||||||
|
search_url = "https://api.github.com/search/issues"
|
||||||
|
repo_name = f"{repo_owner}/{repo_name}"
|
||||||
|
params = {
|
||||||
|
"q": f'repo:{repo_name} is:issue "{title}"'.replace(" ", "%20"),
|
||||||
|
"sort": "created",
|
||||||
|
"order": "desc",
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {"Authorization": f"token {token}", "Accept": "application/vnd.github.v3+json", **HEADERS}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Build query string manually
|
||||||
|
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
||||||
|
full_url = f"{search_url}?{query_string}"
|
||||||
|
|
||||||
|
req = request.Request(full_url, headers=headers)
|
||||||
|
with request.urlopen(req) as response:
|
||||||
|
data = json.loads(response.read().decode("utf-8"))
|
||||||
|
|
||||||
|
# Check if any existing issues match our pattern
|
||||||
|
for issue in data.get("items", []):
|
||||||
|
issue_title = issue["title"].lower()
|
||||||
|
if "supported versions" in issue_title and ("[maint]" in issue_title or "maint" in issue_title):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not check existing issues: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_github_issue(title: str, body: str, repo_owner: str, repo_name: str, token: str) -> bool:
|
||||||
|
url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/issues"
|
||||||
|
|
||||||
|
data = {"title": title, "body": body, "labels": ["maintenance", "dependencies"]}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
"Accept": "application/vnd.github.v3+json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
**HEADERS,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
req = request.Request(url, data=json.dumps(data).encode("utf-8"), headers=headers, method="POST")
|
||||||
|
with request.urlopen(req) as response:
|
||||||
|
if response.status == 201:
|
||||||
|
issue_data = json.loads(response.read().decode("utf-8"))
|
||||||
|
print(f"✅ Created GitHub issue: {issue_data['html_url']}")
|
||||||
|
return True
|
||||||
|
print(f"❌ Failed to create issue. Status: {response.status}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error creating GitHub issue: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def generate_issue_body(differences: VersionDifferences, _current: VersionMapping, expected: VersionMapping) -> str:
|
||||||
|
body = "## Supported versions need updating\n\n"
|
||||||
|
body += (
|
||||||
|
"The supported Python/Django version combinations have changed and "
|
||||||
|
"need to be updated in the documentation.\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if differences.added_python_versions:
|
||||||
|
body += "### Added Python versions\n"
|
||||||
|
for version in differences.added_python_versions:
|
||||||
|
version_str = env_format(version, divider=".")
|
||||||
|
body += f"- Python {version_str}\n"
|
||||||
|
body += "\n"
|
||||||
|
|
||||||
|
if differences.removed_python_versions:
|
||||||
|
body += "### Removed Python versions\n"
|
||||||
|
for version in differences.removed_python_versions:
|
||||||
|
version_str = env_format(version, divider=".")
|
||||||
|
body += f"- Python {version_str}\n"
|
||||||
|
body += "\n"
|
||||||
|
|
||||||
|
if differences.changed_django_versions:
|
||||||
|
body += "### Changed Django version support\n"
|
||||||
|
for python_version, changes in differences.changed_django_versions.items():
|
||||||
|
python_str = env_format(python_version, divider=".")
|
||||||
|
body += f"**Python {python_str}:**\n"
|
||||||
|
if changes.added:
|
||||||
|
for django_version in changes.added:
|
||||||
|
django_str = env_format(django_version, divider=".")
|
||||||
|
body += f"- ✅ Added Django {django_str}\n"
|
||||||
|
if changes.removed:
|
||||||
|
for django_version in changes.removed:
|
||||||
|
django_str = env_format(django_version, divider=".")
|
||||||
|
body += f"- ❌ Removed Django {django_str}\n"
|
||||||
|
body += "\n"
|
||||||
|
|
||||||
|
body += "### Expected compatibility table\n\n"
|
||||||
|
body += build_readme(expected)
|
||||||
|
body += "\n\n"
|
||||||
|
|
||||||
|
body += "### Files to update\n"
|
||||||
|
body += "- `docs/overview/compatibility.md`\n"
|
||||||
|
body += "- `tox.ini`\n"
|
||||||
|
body += "- `pyproject.toml`\n"
|
||||||
|
body += "- `.github/workflows/tests.yml`\n\n"
|
||||||
|
|
||||||
|
body += "Run `python scripts/supported_versions.py generate` to get the updated configurations."
|
||||||
|
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# MAIN
|
||||||
|
######################################
|
||||||
|
|
||||||
|
|
||||||
|
def create_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Manage supported Python/Django version combinations",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog=textwrap.dedent("""
|
||||||
|
Commands:
|
||||||
|
generate Generate configuration for current supported versions
|
||||||
|
check Check if versions need updating and create GitHub issue
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
GITHUB_TOKEN Required for 'check' command to create GitHub issues
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
python scripts/supported_versions.py generate
|
||||||
|
GITHUB_TOKEN=your_token python scripts/supported_versions.py check
|
||||||
|
"""),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("command", choices=["generate", "check"], help="Command to execute")
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--repo-owner", default="django-components", help="GitHub repository owner (default: django-components)"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--repo-name", default="django-components", help="GitHub repository name (default: django-components)"
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = create_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.command == "generate":
|
||||||
|
command_generate()
|
||||||
|
elif args.command == "check":
|
||||||
|
command_check(args.repo_owner, args.repo_name)
|
||||||
|
else:
|
||||||
|
parser.error(f"Invalid command: {args.command}")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n❌ Interrupted by user")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Unexpected error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue