diff options
| author | Natalia <124304+nessita@users.noreply.github.com> | 2026-05-25 19:53:17 -0300 |
|---|---|---|
| committer | nessita <124304+nessita@users.noreply.github.com> | 2026-05-27 11:10:31 -0300 |
| commit | 7ae70e7504b98553ab3537be84f930ea7e28c3c9 (patch) | |
| tree | 26226b4f5b96604565c5db1c0dc86b1778ee0d16 | |
| parent | 7436661b9c7f7232cc416720825561b2fb0a6649 (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-x | scripts/do_django_release.py | 290 | ||||
| -rw-r--r-- | scripts/tests.py | 99 |
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"] |
