Skip to content

Commit daed151

Browse files
authored
Merge pull request #76 from smkent/manage-release
Implement manage-cookie release for releasing new patch versions
2 parents ee4514f + bb1f6f5 commit daed151

File tree

6 files changed

+150
-12
lines changed

6 files changed

+150
-12
lines changed

cookie_python/manage/main.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from enum import Enum
55
from typing import Callable, Optional
66

7+
from .release import release_action
78
from .update import update_action
89

910

@@ -13,6 +14,11 @@ class Action(str, Enum):
1314
update_action,
1415
"Update repository cruft and dependencies",
1516
)
17+
RELEASE = (
18+
"release",
19+
release_action,
20+
"Release a new patch version",
21+
)
1622

1723
def __new__(
1824
cls,
@@ -31,8 +37,14 @@ def main() -> None:
3137
ap = argparse.ArgumentParser()
3238
ap.add_argument(
3339
"action",
34-
type=lambda value: Action(str(value)),
40+
type=Action,
3541
choices=list(Action),
42+
metavar="|".join([a.value for a in Action]),
43+
help="Action to perform ("
44+
+ ", ".join(
45+
[f"{a.value}: {a.description}" for a in Action] # type: ignore
46+
)
47+
+ ")",
3648
)
3749
ap.add_argument("repo", nargs="+", help="Repository URL")
3850
ap.add_argument(

cookie_python/manage/release.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from argparse import Namespace
2+
3+
import semver
4+
5+
from .repo import RepoSandbox
6+
7+
8+
def release_patch_version(repo: RepoSandbox) -> None:
9+
releases = (
10+
repo.run(
11+
["gh", "release", "list"],
12+
capture_output=True,
13+
check=True,
14+
)
15+
.stdout.decode("utf-8")
16+
.splitlines()
17+
)
18+
latest_tag = None
19+
for line in releases:
20+
tag, status, *_ = line.split()
21+
if status.lower() == "latest":
22+
latest_tag = tag
23+
break
24+
if not latest_tag:
25+
print("Unable to find latest version")
26+
return None
27+
check_refs = ["origin/main", latest_tag]
28+
refs = []
29+
for ref in check_refs:
30+
refs.append(
31+
repo.run(["git", "rev-parse", ref], capture_output=True)
32+
.stdout.decode("utf-8")
33+
.strip()
34+
)
35+
if len(refs) == len(check_refs) and len(set(refs)) == 1:
36+
print(f"No new changes since latest release {latest_tag}")
37+
return None
38+
sv = semver.VersionInfo.parse(latest_tag.lstrip("v"))
39+
next_patch_ver = sv.bump_patch()
40+
new_tag = f"v{next_patch_ver}"
41+
if repo.dry_run:
42+
print(f"Would release new version {new_tag}")
43+
return None
44+
repo.run(["gh", "release", "create", new_tag, "--generate-notes"])
45+
return None
46+
47+
48+
def release_action(args: Namespace) -> None:
49+
for repo_url in args.repo:
50+
with RepoSandbox(repo_url, args.dry_run) as repo:
51+
release_patch_version(repo)

poetry.lock

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ packages = [
2525
python = "^3.8"
2626
poetry-dynamic-versioning = "*"
2727
pyyaml = "*"
28+
semver = "*"
2829

2930
[tool.poetry.dev-dependencies]
3031
bandit = {extras = ["toml"], version = "*"}

tests/test_manage_cookie.py

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
import os
33
import subprocess
44
import sys
5+
import textwrap
56
from pathlib import Path
6-
from typing import Iterator
7+
from types import SimpleNamespace
8+
from typing import Any, Dict, Iterable, Iterator, Tuple
79
from unittest.mock import patch
810

911
import pytest
@@ -13,6 +15,7 @@
1315

1416
AUTHOR_NAME = "Ness"
1517
AUTHOR_EMAIL = "ness@onett.example"
18+
PROJECT_NAME = "unit-test-1"
1619

1720

1821
@pytest.fixture
@@ -34,7 +37,6 @@ def environ() -> Iterator[None]:
3437
def new_cookie(
3538
request: pytest.FixtureRequest, environ: None, temp_dir: str
3639
) -> Iterator[Path]:
37-
project_name = "unit-test-1"
3840
testargs = [
3941
"new-cookie",
4042
"--local",
@@ -49,22 +51,51 @@ def new_cookie(
4951
"author_name": AUTHOR_NAME,
5052
"github_user": "ness.unittest.example",
5153
"project_description": "Unit test project",
52-
"project_name": project_name,
54+
"project_name": PROJECT_NAME,
5355
}
5456
),
5557
"-c",
5658
request.param,
5759
]
5860
with patch.object(sys, "argv", testargs):
5961
new_cookie_main()
60-
project_dir = Path(temp_dir) / project_name
62+
yield Path(temp_dir) / PROJECT_NAME
63+
64+
65+
@pytest.fixture
66+
def new_cookie_with_lock(new_cookie: Path, temp_dir: str) -> Iterator[Path]:
6167
for cmd in (
6268
["poetry", "lock", "--no-update"],
6369
["git", "add", "poetry.lock"],
6470
["git", "commit", "-m", "Create `poetry.lock`"],
6571
):
66-
subprocess.run(cmd, cwd=project_dir, check=True)
67-
yield project_dir
72+
subprocess.run(cmd, cwd=new_cookie, check=True)
73+
yield new_cookie
74+
75+
76+
@pytest.fixture
77+
def run_or_mock() -> Iterator[Dict[Tuple[str, ...], str]]:
78+
real_run = subprocess.run
79+
mocked_commands: Dict[Tuple[str, ...], str] = {}
80+
81+
def _run(*args: Any, **kwargs: Any) -> Any:
82+
cmd = args[0]
83+
cmd_tuple = tuple(cmd)
84+
while cmd_tuple:
85+
if cmd_tuple in mocked_commands:
86+
return SimpleNamespace(
87+
stdout=mocked_commands[cmd_tuple].encode()
88+
)
89+
cmd_tuple = cmd_tuple[:-1]
90+
return real_run(*args, **kwargs)
91+
92+
with patch.object(subprocess, "run", _run):
93+
yield mocked_commands
94+
95+
96+
def _manage_cookie(argv: Iterable[str]) -> None:
97+
with patch.object(sys, "argv", argv):
98+
manage_cookie_main()
6899

69100

70101
@pytest.mark.parametrize(
@@ -73,7 +104,30 @@ def new_cookie(
73104
ids=["no_updates", "updates"],
74105
indirect=True,
75106
)
76-
def test_manage_cookie_update(new_cookie: str) -> None:
77-
testargs = ["manage-cookie", "update", str(new_cookie), "-p"]
78-
with patch.object(sys, "argv", testargs):
79-
manage_cookie_main()
107+
def test_manage_cookie_update(new_cookie_with_lock: str) -> None:
108+
_manage_cookie(
109+
["manage-cookie", "update", str(new_cookie_with_lock), "-p"]
110+
)
111+
112+
113+
@pytest.mark.parametrize("add_commit", (True, False))
114+
def test_manage_cookie_release(
115+
new_cookie: str, run_or_mock: Dict[Tuple[str, ...], str], add_commit: bool
116+
) -> None:
117+
run_or_mock[("gh", "release", "list")] = textwrap.dedent(
118+
"""
119+
TITLE TYPE TAG NAME PUBLISHED
120+
v1.1.38 Latest v1.1.38 in a galaxy far, far away
121+
v0.21.87 v0.21.87 about a long time ago
122+
v0.0.1 v0.0.1 about never ago
123+
"""
124+
).strip()
125+
run_or_mock[("gh",)] = "" # Prevent any actual gh invocations
126+
subprocess.run(["git", "tag", "v1.1.38", "@"], cwd=new_cookie, check=True)
127+
if add_commit:
128+
subprocess.run(
129+
["git", "commit", "--allow-empty", "-m", "create empty commit"],
130+
cwd=new_cookie,
131+
check=True,
132+
)
133+
_manage_cookie(["manage-cookie", "release", str(new_cookie), "-p"])

types/semver.pyi

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from __future__ import annotations
2+
3+
class VersionInfo:
4+
@staticmethod
5+
def parse(version: str) -> VersionInfo:
6+
pass
7+
def bump_patch(self) -> VersionInfo:
8+
pass

0 commit comments

Comments
 (0)