Skip to content

Commit c6a4d1e

Browse files
authored
Check CPython repo branch and age (python#334)
1 parent 971a2d1 commit c6a4d1e

File tree

2 files changed

+127
-11
lines changed

2 files changed

+127
-11
lines changed

run_release.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,36 @@ def run_blurb_release(db: ReleaseShelf) -> None:
424424
)
425425

426426

427+
def check_cpython_repo_branch(db: ReleaseShelf) -> None:
428+
current_branch = subprocess.check_output(
429+
shlex.split("git branch --show-current"), text=True, cwd=db["git_repo"]
430+
).strip()
431+
expected_branch = db["release"].branch
432+
if current_branch != expected_branch:
433+
raise ReleaseException(
434+
f"CPython repository is on {current_branch} branch, "
435+
f"expected {expected_branch}"
436+
)
437+
438+
439+
def check_cpython_repo_age(db: ReleaseShelf) -> None:
440+
# %ct = committer date, UNIX timestamp (for example, "1768300016")
441+
timestamp = subprocess.check_output(
442+
shlex.split('git log -1 --format="%ct"'), text=True, cwd=db["git_repo"]
443+
).strip()
444+
age_seconds = time.time() - int(timestamp.strip())
445+
is_old = age_seconds > 86400 # 1 day
446+
447+
# cr = committer date, relative (for example, "3 days ago")
448+
out = subprocess.check_output(
449+
shlex.split('git log -1 --format="%cr"'), text=True, cwd=db["git_repo"]
450+
)
451+
print(f"Last CPython commit was {out.strip()}")
452+
453+
if is_old and not ask_question("Continue with old repo?"):
454+
raise ReleaseException("CPython repository is old")
455+
456+
427457
def check_cpython_repo_is_clean(db: ReleaseShelf) -> None:
428458
if subprocess.check_output(["git", "status", "--porcelain"], cwd=db["git_repo"]):
429459
raise ReleaseException("Git repository is not clean")
@@ -1386,23 +1416,25 @@ def _api_key(api_key: str) -> str:
13861416
),
13871417
Task(check_sigstore_client, "Checking Sigstore CLI"),
13881418
Task(check_buildbots, "Check buildbots are good"),
1389-
Task(check_cpython_repo_is_clean, "Checking Git repository is clean"),
1419+
Task(check_cpython_repo_branch, "Checking CPython repository branch"),
1420+
Task(check_cpython_repo_age, "Checking CPython repository age"),
1421+
Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"),
13901422
*(
13911423
[Task(check_magic_number, "Checking the magic number is up-to-date")]
13921424
if magic
13931425
else []
13941426
),
13951427
Task(prepare_temporary_branch, "Checking out a temporary release branch"),
13961428
Task(run_blurb_release, "Run blurb release"),
1397-
Task(check_cpython_repo_is_clean, "Checking Git repository is clean"),
1429+
Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"),
13981430
Task(prepare_pydoc_topics, "Preparing pydoc topics"),
13991431
Task(bump_version, "Bump version"),
14001432
Task(bump_version_in_docs, "Bump version in docs"),
1401-
Task(check_cpython_repo_is_clean, "Checking Git repository is clean"),
1433+
Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"),
14021434
Task(run_autoconf, "Running autoconf"),
1403-
Task(check_cpython_repo_is_clean, "Checking Git repository is clean"),
1435+
Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"),
14041436
Task(check_pyspecific, "Checking pyspecific"),
1405-
Task(check_cpython_repo_is_clean, "Checking Git repository is clean"),
1437+
Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"),
14061438
Task(create_tag, "Create tag"),
14071439
Task(push_to_local_fork, "Push new tags and branches to private fork"),
14081440
Task(start_build_release, "Start the build-release workflow"),

tests/test_run_release.py

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
import contextlib
33
import io
44
import tarfile
5+
from contextlib import nullcontext as does_not_raise
56
from pathlib import Path
67
from typing import cast
78

89
import pytest
910

1011
import run_release
1112
from release import ReleaseShelf, Tag
13+
from run_release import ReleaseException
1214

1315

1416
@pytest.mark.parametrize(
@@ -26,8 +28,7 @@ def test_check_sigstore_version_success(version) -> None:
2628
)
2729
def test_check_sigstore_version_exception(version) -> None:
2830
with pytest.raises(
29-
run_release.ReleaseException,
30-
match="Sigstore version not detected or not valid",
31+
ReleaseException, match="Sigstore version not detected or not valid"
3132
):
3233
run_release.check_sigstore_version(version)
3334

@@ -46,21 +47,104 @@ def test_extract_github_owner(url: str, expected: str) -> None:
4647

4748
def test_invalid_extract_github_owner() -> None:
4849
with pytest.raises(
49-
run_release.ReleaseException,
50+
ReleaseException,
5051
match="Could not parse GitHub owner from 'origin' remote URL: "
5152
"https://example.com",
5253
):
5354
run_release.extract_github_owner("https://example.com")
5455

5556

57+
@pytest.mark.parametrize(
58+
["release_tag", "git_current_branch", "expectation"],
59+
[
60+
# Success cases
61+
("3.15.0rc1", "3.15\n", does_not_raise()),
62+
("3.15.0b1", "3.15\n", does_not_raise()),
63+
("3.15.0a6", "main\n", does_not_raise()),
64+
("3.14.3", "3.14\n", does_not_raise()),
65+
("3.13.12", "3.13\n", does_not_raise()),
66+
# Failure cases
67+
(
68+
"3.15.0rc1",
69+
"main\n",
70+
pytest.raises(ReleaseException, match="on main branch, expected 3.15"),
71+
),
72+
(
73+
"3.15.0b1",
74+
"main\n",
75+
pytest.raises(ReleaseException, match="on main branch, expected 3.15"),
76+
),
77+
(
78+
"3.15.0a6",
79+
"3.14\n",
80+
pytest.raises(ReleaseException, match="on 3.14 branch, expected main"),
81+
),
82+
(
83+
"3.14.3",
84+
"main\n",
85+
pytest.raises(ReleaseException, match="on main branch, expected 3.14"),
86+
),
87+
],
88+
)
89+
def test_check_cpython_repo_branch(
90+
monkeypatch, release_tag: str, git_current_branch: str, expectation
91+
) -> None:
92+
# Arrange
93+
db = {"release": Tag(release_tag), "git_repo": "/fake/repo"}
94+
monkeypatch.setattr(
95+
run_release.subprocess,
96+
"check_output",
97+
lambda *args, **kwargs: git_current_branch,
98+
)
99+
100+
# Act / Assert
101+
with expectation:
102+
run_release.check_cpython_repo_branch(cast(ReleaseShelf, db))
103+
104+
105+
@pytest.mark.parametrize(
106+
["age_seconds", "user_continues", "expectation"],
107+
[
108+
# Recent repo (< 1 day) - no question asked
109+
(3600, None, does_not_raise()),
110+
# Old repo (> 1 day) + user says yes
111+
(90000, True, does_not_raise()),
112+
# Old repo (> 1 day) + user says no
113+
(90000, False, pytest.raises(ReleaseException, match="repository is old")),
114+
],
115+
)
116+
def test_check_cpython_repo_age(
117+
monkeypatch, age_seconds: int, user_continues: bool | None, expectation
118+
) -> None:
119+
# Arrange
120+
db = {"release": Tag("3.15.0a6"), "git_repo": "/fake/repo"}
121+
current_time = 1700000000
122+
commit_timestamp = current_time - age_seconds
123+
124+
def fake_check_output(cmd, **kwargs):
125+
cmd_str = " ".join(cmd)
126+
if "%ct" in cmd_str:
127+
return f"{commit_timestamp}\n"
128+
if "%cr" in cmd_str:
129+
return "some time ago\n"
130+
return ""
131+
132+
monkeypatch.setattr(run_release.subprocess, "check_output", fake_check_output)
133+
monkeypatch.setattr(run_release.time, "time", lambda: current_time)
134+
if user_continues is not None:
135+
monkeypatch.setattr(run_release, "ask_question", lambda _: user_continues)
136+
137+
# Act / Assert
138+
with expectation:
139+
run_release.check_cpython_repo_age(cast(ReleaseShelf, db))
140+
141+
56142
def test_check_magic_number() -> None:
57143
db = {
58144
"release": Tag("3.14.0rc1"),
59145
"git_repo": str(Path(__file__).parent / "magicdata"),
60146
}
61-
with pytest.raises(
62-
run_release.ReleaseException, match="Magic numbers in .* don't match"
63-
):
147+
with pytest.raises(ReleaseException, match="Magic numbers in .* don't match"):
64148
run_release.check_magic_number(cast(ReleaseShelf, db))
65149

66150

0 commit comments

Comments
 (0)