summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNatalia <124304+nessita@users.noreply.github.com>2026-05-25 19:53:17 -0300
committernessita <124304+nessita@users.noreply.github.com>2026-05-27 11:10:31 -0300
commit7ae70e7504b98553ab3537be84f930ea7e28c3c9 (patch)
tree26226b4f5b96604565c5db1c0dc86b1778ee0d16
parent7436661b9c7f7232cc416720825561b2fb0a6649 (diff)
Refactored release script to support testing in isolation.
Extracted helpers (get_commit_hash, parse_major_version, find_release_artifacts, create_checksum_file) and isolated all code with side-effects in main() guarded by __name__ == "__main__", allowing the module to be imported without starting the release process. Added tests for the new helpers in scripts/tests.py. Thanks Jacob Walls for the review and IRL test.
-rwxr-xr-xscripts/do_django_release.py290
-rw-r--r--scripts/tests.py99
2 files changed, 262 insertions, 127 deletions
diff --git a/scripts/do_django_release.py b/scripts/do_django_release.py
index 89292411f0..8f7450147e 100755
--- a/scripts/do_django_release.py
+++ b/scripts/do_django_release.py
@@ -13,22 +13,6 @@ import re
import subprocess
from datetime import date
-PGP_KEY_ID = os.getenv("PGP_KEY_ID")
-PGP_KEY_URL = os.getenv("PGP_KEY_URL")
-PGP_EMAIL = os.getenv("PGP_EMAIL")
-DEST_FOLDER = os.path.expanduser(os.getenv("DEST_FOLDER"))
-
-assert (
- PGP_KEY_ID
-), "Missing PGP_KEY_ID: Set this env var to your PGP key ID (used for signing)."
-assert (
- PGP_KEY_URL
-), "Missing PGP_KEY_URL: Set this env var to your PGP public key URL (for fetching)."
-assert DEST_FOLDER and os.path.exists(
- DEST_FOLDER
-), "Missing DEST_FOLDER: Set this env var to the local path to place the artifacts."
-
-
checksum_file_text = """This file contains MD5, SHA1, and SHA256 checksums for the
source-code tarball and wheel files of Django {django_version}, released {release_date}.
@@ -93,141 +77,193 @@ def build_artifacts():
build_main([])
-def do_checksum(checksum_algo, release_file):
+def do_checksum(checksum_algo, release_file, dist_path):
with open(os.path.join(dist_path, release_file), "rb") as f:
return checksum_algo(f.read()).hexdigest()
-# Ensure the working directory is clean.
-subprocess.call(["git", "clean", "-fdx"])
+def get_commit_hash():
+ return subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
-commit_hash = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
-django_repo_path = os.path.abspath(os.path.curdir)
-dist_path = os.path.join(django_repo_path, "dist")
+def parse_major_version(django_version):
+ major = ".".join(django_version.split(".")[:2])
+ match = re.search("[abrc]", major)
+ if match:
+ major = major[: match.start()]
+ return major
-# Build release files.
-build_artifacts()
-release_files = os.listdir(dist_path)
-wheel_name = None
-tarball_name = None
-for f in release_files:
- if f.endswith(".whl"):
- wheel_name = f
- if f.endswith(".tar.gz"):
- tarball_name = f
-assert wheel_name is not None
-assert tarball_name is not None
+def find_release_artifacts(dist_path):
+ wheel_name = None
+ tarball_name = None
+ for f in os.listdir(dist_path):
+ if f.endswith(".whl"):
+ wheel_name = f
+ elif f.endswith(".tar.gz"):
+ tarball_name = f
+ return wheel_name, tarball_name
-django_version = wheel_name.split("-")[1]
-django_major_version = ".".join(django_version.split(".")[:2])
-artifacts_path = os.path.join(os.path.expanduser(DEST_FOLDER), django_version)
-os.makedirs(artifacts_path, exist_ok=True)
+def create_checksum_file(
+ *,
+ django_version,
+ release_date,
+ checksum_file_path,
+ tarball_name,
+ wheel_name,
+ commit_hash,
+ dist_path,
+ pgp_key_id,
+ pgp_key_url,
+):
+ kwargs = dict(
+ release_date=release_date,
+ pgp_key_id=pgp_key_id,
+ django_version=django_version,
+ pgp_key_url=pgp_key_url,
+ checksum_file_name=os.path.basename(checksum_file_path),
+ wheel_name=wheel_name,
+ tarball_name=tarball_name,
+ commit_hash=commit_hash,
+ )
+ for checksum_name, checksum_algo in (
+ ("md5", hashlib.md5),
+ ("sha1", hashlib.sha1),
+ ("sha256", hashlib.sha256),
+ ):
+ kwargs[f"{checksum_name}_tarball"] = do_checksum(
+ checksum_algo, tarball_name, dist_path
+ )
+ kwargs[f"{checksum_name}_wheel"] = do_checksum(
+ checksum_algo, wheel_name, dist_path
+ )
+ with open(checksum_file_path, "wb") as f:
+ f.write(checksum_file_text.format(**kwargs).encode("ascii"))
-# Chop alpha/beta/rc suffix
-match = re.search("[abrc]", django_major_version)
-if match:
- django_major_version = django_major_version[: match.start()]
-release_date = date.today().strftime("%B %-d, %Y")
-checksum_file_name = f"Django-{django_version}.checksum.txt"
-checksum_file_kwargs = dict(
- release_date=release_date,
- pgp_key_id=PGP_KEY_ID,
- django_version=django_version,
- pgp_key_url=PGP_KEY_URL,
- checksum_file_name=checksum_file_name,
- wheel_name=wheel_name,
- tarball_name=tarball_name,
- commit_hash=commit_hash,
-)
-checksums = (
- ("md5", hashlib.md5),
- ("sha1", hashlib.sha1),
- ("sha256", hashlib.sha256),
-)
-for checksum_name, checksum_algo in checksums:
- checksum_file_kwargs[f"{checksum_name}_tarball"] = do_checksum(
- checksum_algo, tarball_name
- )
- checksum_file_kwargs[f"{checksum_name}_wheel"] = do_checksum(
- checksum_algo, wheel_name
+def main():
+ pgp_key_id = os.getenv("PGP_KEY_ID")
+ pgp_key_url = os.getenv("PGP_KEY_URL")
+ pgp_email = os.getenv("PGP_EMAIL")
+ dest_folder = os.path.expanduser(os.getenv("DEST_FOLDER"))
+
+ assert (
+ pgp_key_id
+ ), "Missing PGP_KEY_ID: Set this env var to your PGP key ID (used for signing)."
+ assert (
+ pgp_key_url
+ ), "Missing PGP_KEY_URL: Set this env var to your PGP public key URL."
+ assert dest_folder and os.path.exists(
+ dest_folder
+ ), "Missing DEST_FOLDER: Set this env var to the path to place the artifacts."
+
+ # Ensure the working directory is clean.
+ subprocess.call(["git", "clean", "-fdx"])
+
+ commit_hash = get_commit_hash()
+
+ django_repo_path = os.path.abspath(os.path.curdir)
+ dist_path = os.path.join(django_repo_path, "dist")
+
+ # Build release files.
+ build_artifacts()
+ wheel_name, tarball_name = find_release_artifacts(dist_path)
+
+ assert wheel_name is not None
+ assert tarball_name is not None
+
+ django_version = wheel_name.split("-")[1]
+ django_major_version = parse_major_version(django_version)
+ artifacts_path = os.path.join(dest_folder, django_version)
+ os.makedirs(artifacts_path, exist_ok=True)
+ release_date = date.today().strftime("%B %-d, %Y")
+ checksum_file_path = os.path.join(
+ artifacts_path, f"Django-{django_version}.checksum.txt"
)
-# Create the checksum file
-checksum_file_text = checksum_file_text.format(**checksum_file_kwargs)
-checksum_file_path = os.path.join(artifacts_path, checksum_file_name)
-with open(checksum_file_path, "wb") as f:
- f.write(checksum_file_text.encode("ascii"))
+ create_checksum_file(
+ django_version=django_version,
+ release_date=release_date,
+ checksum_file_path=checksum_file_path,
+ wheel_name=wheel_name,
+ tarball_name=tarball_name,
+ commit_hash=commit_hash,
+ dist_path=dist_path,
+ pgp_key_id=pgp_key_id,
+ pgp_key_url=pgp_key_url,
+ )
-print("\n\nDiffing release with checkout for sanity check.")
+ print("\n\nDiffing release with checkout for sanity check.")
-# Unzip and diff...
-unzip_command = [
- "unzip",
- "-q",
- os.path.join(dist_path, wheel_name),
- "-d",
- os.path.join(dist_path, django_major_version),
-]
-subprocess.run(unzip_command)
-diff_command = [
- "diff",
- "-qr",
- "./django/",
- os.path.join(dist_path, django_major_version, "django"),
-]
-subprocess.run(diff_command)
-subprocess.run(
- [
- "rm",
- "-rf",
+ # Unzip and diff...
+ unzip_command = [
+ "unzip",
+ "-q",
+ os.path.join(dist_path, wheel_name),
+ "-d",
os.path.join(dist_path, django_major_version),
]
-)
+ subprocess.run(unzip_command)
+ diff_command = [
+ "diff",
+ "-qr",
+ "./django/",
+ os.path.join(dist_path, django_major_version, "django"),
+ ]
+ subprocess.run(diff_command)
+ subprocess.run(
+ [
+ "rm",
+ "-rf",
+ os.path.join(dist_path, django_major_version),
+ ]
+ )
-print("\n\n=> Commands to run NOW:")
+ print("\n\n=> Commands to run NOW:")
-# Sign the checksum file, this may prompt for a passphrase.
-pgp_email = f"-u {PGP_EMAIL} " if PGP_EMAIL else ""
-print(f"gpg --clearsign {pgp_email}--digest-algo SHA256 {checksum_file_path}")
-# Create, verify and push tag
-print(f'git tag --sign --message="Tag {django_version}" {django_version}')
-print(f"git tag --verify {django_version}")
+ # Sign the checksum file, this may prompt for a passphrase.
+ pgp_email_flag = f"-u {pgp_email} " if pgp_email else ""
+ print(f"gpg --clearsign {pgp_email_flag}--digest-algo SHA256 {checksum_file_path}")
+ # Create, verify and push tag.
+ print(f'git tag --sign --message="Tag {django_version}" {django_version}')
+ print(f"git tag --verify {django_version}")
-# Copy binaries outside the current repo tree to avoid lossing them.
-subprocess.run(["cp", "-r", dist_path, artifacts_path])
+ # Copy binaries outside the current repo tree to avoid lossing them.
+ subprocess.run(["cp", "-r", dist_path, artifacts_path])
+
+ # Make the binaries available to the world
+ print(
+ "\n\n=> These ONLY 15 MINUTES BEFORE RELEASE TIME (consider new terminal "
+ "session with isolated venv)!"
+ )
+
+ # Upload the checksum file and artifacts to the djangoproject admin.
+ print(
+ "\n==> ACTION Add tarball, wheel, and checksum files to the Release entry at:"
+ f"https://www.djangoproject.com/admin/releases/release/{django_version}"
+ )
+ print(
+ f"* Tarball and wheel from {artifacts_path}\n"
+ f"* Signed checksum {checksum_file_path}.asc"
+ )
-# Make the binaries available to the world
-print(
- "\n\n=> These ONLY 15 MINUTES BEFORE RELEASE TIME (consider new terminal "
- "session with isolated venv)!"
-)
+ # Verify the release artifacts (GPG signature, checksums, and smoke test).
+ print("\n==> ACTION Verify the release artifacts:")
+ print(f"VERSION={django_version} verify_release.sh")
-# Upload the checksum file and release artifacts to the djangoproject admin.
-print(
- "\n==> ACTION Add tarball, wheel, and checksum files to the Release entry at:"
- f"https://www.djangoproject.com/admin/releases/release/{django_version}"
-)
-print(
- f"* Tarball and wheel from {artifacts_path}\n"
- f"* Signed checksum {checksum_file_path}.asc"
-)
+ # Upload to PyPI.
+ print("\n==> ACTION Upload to PyPI, ensure your release venv is activated:")
+ print(f"cd {artifacts_path}")
+ print("pip install -U pip twine")
+ print("twine upload --repository django dist/*")
-# Verify the release artifacts (GPG signature, checksums, and smoke test).
-print("\n==> ACTION Verify the release artifacts:")
-print(f"VERSION={django_version} verify_release.sh")
+ # Push the tags.
+ print("\n==> ACTION Push the tags:")
+ print("git push --tags")
-# Upload to PyPI.
-print("\n==> ACTION Upload to PyPI, ensure your release venv is activated:")
-print(f"cd {artifacts_path}")
-print("pip install -U pip twine")
-print("twine upload --repository django dist/*")
+ print("\n\nDONE!!!")
-# Push the tags.
-print("\n==> ACTION Push the tags:")
-print("git push --tags")
-print("\n\nDONE!!!")
+if __name__ == "__main__":
+ main()
diff --git a/scripts/tests.py b/scripts/tests.py
index b36c293703..10afce6588 100644
--- a/scripts/tests.py
+++ b/scripts/tests.py
@@ -12,11 +12,110 @@ Or from the repo root with:
$ PYTHONPATH=scripts/ python -m unittest scripts/tests.py
"""
+import hashlib
+import os
+import tempfile
import unittest
+from do_django_release import (
+ create_checksum_file,
+ find_release_artifacts,
+ parse_major_version,
+)
from prepare_commit_msg import process_commit_message
+class ParseMajorVersionTests(unittest.TestCase):
+ def test_final_patch_release(self):
+ self.assertEqual(parse_major_version("5.2.4"), "5.2")
+
+ def test_final_dot_zero_release(self):
+ self.assertEqual(parse_major_version("6.0"), "6.0")
+
+ def test_alpha(self):
+ self.assertEqual(parse_major_version("6.0a1"), "6.0")
+
+ def test_beta(self):
+ self.assertEqual(parse_major_version("6.0b1"), "6.0")
+
+ def test_release_candidate(self):
+ self.assertEqual(parse_major_version("6.0rc1"), "6.0")
+
+ def test_two_digit_minor(self):
+ self.assertEqual(parse_major_version("5.10a1"), "5.10")
+
+
+class CreateChecksumFileTests(unittest.TestCase):
+ WHEEL_CONTENT = b"fake wheel content"
+ TARBALL_CONTENT = b"fake tarball content"
+
+ def generate_checksum_file(self, **overrides):
+ with tempfile.TemporaryDirectory() as tmp:
+ dist_path = os.path.join(tmp, "dist")
+ os.mkdir(dist_path)
+ with open(
+ os.path.join(dist_path, "Django-5.2.4-py3-none-any.whl"), "wb"
+ ) as f:
+ f.write(self.WHEEL_CONTENT)
+ with open(os.path.join(dist_path, "django-5.2.4.tar.gz"), "wb") as f:
+ f.write(self.TARBALL_CONTENT)
+ artifacts_path = os.path.join(tmp, "artifacts")
+ os.mkdir(artifacts_path)
+ checksum_file_path = os.path.join(
+ artifacts_path, "Django-5.2.4.checksum.txt"
+ )
+ kwargs = dict(
+ django_version="5.2.4",
+ release_date="May 7, 2025",
+ checksum_file_path=checksum_file_path,
+ wheel_name="Django-5.2.4-py3-none-any.whl",
+ tarball_name="django-5.2.4.tar.gz",
+ commit_hash="abc123def456abc123def456abc123def456abc1",
+ dist_path=dist_path,
+ pgp_key_id="ABCD1234ABCD1234",
+ pgp_key_url="https://github.com/releaser.gpg",
+ )
+ kwargs.update(overrides)
+ create_checksum_file(**kwargs)
+ with open(checksum_file_path) as f:
+ return f.read()
+
+ def test_release_metadata(self):
+ result = self.generate_checksum_file()
+ self.assertIn("Django 5.2.4", result)
+ self.assertIn("May 7, 2025", result)
+ self.assertIn("ABCD1234ABCD1234", result)
+ self.assertIn("https://github.com/releaser.gpg", result)
+ self.assertIn("Django-5.2.4.checksum.txt", result)
+ self.assertIn("abc123def456abc123def456abc123def456abc1 5.2.4", result)
+
+ def test_artifact_checksums(self):
+ result = self.generate_checksum_file()
+ for algo in (hashlib.md5, hashlib.sha1, hashlib.sha256):
+ expected_tarball = algo(self.TARBALL_CONTENT).hexdigest()
+ expected_wheel = algo(self.WHEEL_CONTENT).hexdigest()
+ self.assertIn(f"{expected_tarball} django-5.2.4.tar.gz", result)
+ self.assertIn(f"{expected_wheel} Django-5.2.4-py3-none-any.whl", result)
+
+
+class FindReleaseArtifactsTests(unittest.TestCase):
+ def test_finds_wheel_and_tarball(self):
+ with tempfile.TemporaryDirectory() as d:
+ with open(os.path.join(d, "Django-5.2.4-py3-none-any.whl"), "w"):
+ pass
+ with open(os.path.join(d, "django-5.2.4.tar.gz"), "w"):
+ pass
+ wheel, tarball = find_release_artifacts(d)
+ self.assertEqual(wheel, "Django-5.2.4-py3-none-any.whl")
+ self.assertEqual(tarball, "django-5.2.4.tar.gz")
+
+ def test_empty_directory_returns_none(self):
+ with tempfile.TemporaryDirectory() as d:
+ wheel, tarball = find_release_artifacts(d)
+ self.assertIsNone(wheel)
+ self.assertIsNone(tarball)
+
+
class ProcessCommitMessageTests(unittest.TestCase):
def test_non_stable_branch_no_prefix_added(self):
lines = ["Fixed #123 -- Added a feature.\n"]