diff --git a/run_release.py b/run_release.py index b69b2d2e..65953ca1 100755 --- a/run_release.py +++ b/run_release.py @@ -424,6 +424,36 @@ def run_blurb_release(db: ReleaseShelf) -> None: ) +def check_cpython_repo_branch(db: ReleaseShelf) -> None: + current_branch = subprocess.check_output( + shlex.split("git branch --show-current"), text=True, cwd=db["git_repo"] + ).strip() + expected_branch = db["release"].branch + if current_branch != expected_branch: + raise ReleaseException( + f"CPython repository is on {current_branch} branch, " + f"expected {expected_branch}" + ) + + +def check_cpython_repo_age(db: ReleaseShelf) -> None: + # %ct = committer date, UNIX timestamp (for example, "1768300016") + timestamp = subprocess.check_output( + shlex.split('git log -1 --format="%ct"'), text=True, cwd=db["git_repo"] + ).strip() + age_seconds = time.time() - int(timestamp.strip()) + is_old = age_seconds > 86400 # 1 day + + # cr = committer date, relative (for example, "3 days ago") + out = subprocess.check_output( + shlex.split('git log -1 --format="%cr"'), text=True, cwd=db["git_repo"] + ) + print(f"Last CPython commit was {out.strip()}") + + if is_old and not ask_question("Continue with old repo?"): + raise ReleaseException("CPython repository is old") + + def check_cpython_repo_is_clean(db: ReleaseShelf) -> None: if subprocess.check_output(["git", "status", "--porcelain"], cwd=db["git_repo"]): raise ReleaseException("Git repository is not clean") @@ -1381,7 +1411,9 @@ def _api_key(api_key: str) -> str: ), Task(check_sigstore_client, "Checking Sigstore CLI"), Task(check_buildbots, "Check buildbots are good"), - Task(check_cpython_repo_is_clean, "Checking Git repository is clean"), + Task(check_cpython_repo_branch, "Checking CPython repository branch"), + Task(check_cpython_repo_age, "Checking CPython repository age"), + Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"), *( [Task(check_magic_number, "Checking the magic number is up-to-date")] if magic @@ -1389,15 +1421,15 @@ def _api_key(api_key: str) -> str: ), Task(prepare_temporary_branch, "Checking out a temporary release branch"), Task(run_blurb_release, "Run blurb release"), - Task(check_cpython_repo_is_clean, "Checking Git repository is clean"), + Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"), Task(prepare_pydoc_topics, "Preparing pydoc topics"), Task(bump_version, "Bump version"), Task(bump_version_in_docs, "Bump version in docs"), - Task(check_cpython_repo_is_clean, "Checking Git repository is clean"), + Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"), Task(run_autoconf, "Running autoconf"), - Task(check_cpython_repo_is_clean, "Checking Git repository is clean"), + Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"), Task(check_pyspecific, "Checking pyspecific"), - Task(check_cpython_repo_is_clean, "Checking Git repository is clean"), + Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"), Task(create_tag, "Create tag"), Task(push_to_local_fork, "Push new tags and branches to private fork"), Task(start_build_release, "Start the build-release workflow"), diff --git a/tests/test_run_release.py b/tests/test_run_release.py index 47864f52..a8790ab6 100644 --- a/tests/test_run_release.py +++ b/tests/test_run_release.py @@ -2,6 +2,7 @@ import contextlib import io import tarfile +from contextlib import nullcontext as does_not_raise from pathlib import Path from typing import cast @@ -9,6 +10,7 @@ import run_release from release import ReleaseShelf, Tag +from run_release import ReleaseException @pytest.mark.parametrize( @@ -26,8 +28,7 @@ def test_check_sigstore_version_success(version) -> None: ) def test_check_sigstore_version_exception(version) -> None: with pytest.raises( - run_release.ReleaseException, - match="Sigstore version not detected or not valid", + ReleaseException, match="Sigstore version not detected or not valid" ): run_release.check_sigstore_version(version) @@ -46,21 +47,104 @@ def test_extract_github_owner(url: str, expected: str) -> None: def test_invalid_extract_github_owner() -> None: with pytest.raises( - run_release.ReleaseException, + ReleaseException, match="Could not parse GitHub owner from 'origin' remote URL: " "https://example.com", ): run_release.extract_github_owner("https://example.com") +@pytest.mark.parametrize( + ["release_tag", "git_current_branch", "expectation"], + [ + # Success cases + ("3.15.0rc1", "3.15\n", does_not_raise()), + ("3.15.0b1", "3.15\n", does_not_raise()), + ("3.15.0a6", "main\n", does_not_raise()), + ("3.14.3", "3.14\n", does_not_raise()), + ("3.13.12", "3.13\n", does_not_raise()), + # Failure cases + ( + "3.15.0rc1", + "main\n", + pytest.raises(ReleaseException, match="on main branch, expected 3.15"), + ), + ( + "3.15.0b1", + "main\n", + pytest.raises(ReleaseException, match="on main branch, expected 3.15"), + ), + ( + "3.15.0a6", + "3.14\n", + pytest.raises(ReleaseException, match="on 3.14 branch, expected main"), + ), + ( + "3.14.3", + "main\n", + pytest.raises(ReleaseException, match="on main branch, expected 3.14"), + ), + ], +) +def test_check_cpython_repo_branch( + monkeypatch, release_tag: str, git_current_branch: str, expectation +) -> None: + # Arrange + db = {"release": Tag(release_tag), "git_repo": "/fake/repo"} + monkeypatch.setattr( + run_release.subprocess, + "check_output", + lambda *args, **kwargs: git_current_branch, + ) + + # Act / Assert + with expectation: + run_release.check_cpython_repo_branch(cast(ReleaseShelf, db)) + + +@pytest.mark.parametrize( + ["age_seconds", "user_continues", "expectation"], + [ + # Recent repo (< 1 day) - no question asked + (3600, None, does_not_raise()), + # Old repo (> 1 day) + user says yes + (90000, True, does_not_raise()), + # Old repo (> 1 day) + user says no + (90000, False, pytest.raises(ReleaseException, match="repository is old")), + ], +) +def test_check_cpython_repo_age( + monkeypatch, age_seconds: int, user_continues: bool | None, expectation +) -> None: + # Arrange + db = {"release": Tag("3.15.0a6"), "git_repo": "/fake/repo"} + current_time = 1700000000 + commit_timestamp = current_time - age_seconds + + def fake_check_output(cmd, **kwargs): + cmd_str = " ".join(cmd) + if "%ct" in cmd_str: + return f"{commit_timestamp}\n" + if "%cr" in cmd_str: + return "some time ago\n" + return "" + + monkeypatch.setattr(run_release.subprocess, "check_output", fake_check_output) + monkeypatch.setattr(run_release.time, "time", lambda: current_time) + if user_continues is not None: + monkeypatch.setattr(run_release, "ask_question", lambda _: user_continues) + + # Act / Assert + with expectation: + run_release.check_cpython_repo_age(cast(ReleaseShelf, db)) + + def test_check_magic_number() -> None: db = { "release": Tag("3.14.0rc1"), "git_repo": str(Path(__file__).parent / "magicdata"), } - with pytest.raises( - run_release.ReleaseException, match="Magic numbers in .* don't match" - ): + with pytest.raises(ReleaseException, match="Magic numbers in .* don't match"): run_release.check_magic_number(cast(ReleaseShelf, db))