Skip to content

Commit ed7063a

Browse files
🔖 bump version 0.2.1 -> 0.3.0 (#26)
* add bump command * update command to get url * fix message for changelog * grab latest changes * add dry run * add release command * add force run to some commands * fix quotes? * update CHANGELOG for version 0.3.0 * 🔖 bump version 0.2.1 -> 0.3.0
1 parent 947caea commit ed7063a

File tree

7 files changed

+282
-4
lines changed

7 files changed

+282
-4
lines changed

Diff for: .bin/bump.py

+255
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#!/usr/bin/env -S uv run --quiet
2+
# /// script
3+
# requires-python = ">=3.13"
4+
# dependencies = [
5+
# "bumpver",
6+
# "typer",
7+
# ]
8+
# ///
9+
from __future__ import annotations
10+
11+
import re
12+
import subprocess
13+
import sys
14+
from enum import Enum
15+
from pathlib import Path
16+
from typing import Annotated
17+
from typing import Any
18+
19+
import typer
20+
from typer import Option
21+
22+
23+
class CommandRunner:
24+
def __init__(self, dry_run: bool = False):
25+
self.dry_run = dry_run
26+
27+
def _quote_arg(self, arg: str) -> str:
28+
if " " in arg and not (arg.startswith('"') or arg.startswith("'")):
29+
return f"'{arg}'"
30+
return arg
31+
32+
def _build_command_args(self, **params: Any) -> str:
33+
args = []
34+
for key, value in params.items():
35+
key = key.replace("_", "-")
36+
if isinstance(value, bool) and value:
37+
args.append(f"--{key}")
38+
elif value is not None:
39+
args.extend([f"--{key}", self._quote_arg(str(value))])
40+
return " ".join(args)
41+
42+
def run(
43+
self, cmd: str, name: str, *args: str, force_run: bool = False, **params: Any
44+
) -> str:
45+
command_parts = [cmd, name]
46+
command_parts.extend(self._quote_arg(arg) for arg in args)
47+
if params:
48+
command_parts.append(self._build_command_args(**params))
49+
command = " ".join(command_parts)
50+
print(
51+
f"would run command: {command}"
52+
if self.dry_run and not force_run
53+
else f"running command: {command}"
54+
)
55+
56+
if self.dry_run and not force_run:
57+
return ""
58+
59+
success, output = self._run_command(command)
60+
if not success:
61+
print(f"{cmd} failed: {output}", file=sys.stderr)
62+
raise typer.Exit(1)
63+
return output
64+
65+
def _run_command(self, command: str) -> tuple[bool, str]:
66+
try:
67+
output = subprocess.check_output(
68+
command, shell=True, text=True, stderr=subprocess.STDOUT
69+
).strip()
70+
return True, output
71+
except subprocess.CalledProcessError as e:
72+
return False, e.output
73+
74+
75+
_runner: CommandRunner | None = None
76+
77+
78+
def run(cmd: str, name: str, *args: str, **params: Any) -> str:
79+
if _runner is None:
80+
raise RuntimeError("CommandRunner not initialized. Call init_runner first.")
81+
return _runner.run(cmd, name, *args, **params)
82+
83+
84+
def init_runner(dry_run: bool = False) -> None:
85+
global _runner
86+
_runner = CommandRunner(dry_run)
87+
88+
89+
def get_current_version():
90+
tags = run("git", "tag", "--sort=-creatordate", force_run=True).splitlines()
91+
return tags[0] if tags else ""
92+
93+
94+
def get_new_version(version: Version, tag: Tag | None = None) -> str:
95+
output = run(
96+
"bumpver", "update", dry=True, tag=tag, force_run=True, **{version: True}
97+
)
98+
if match := re.search(r"New Version: (.+)", output):
99+
return match.group(1)
100+
return typer.prompt("Failed to get new version. Enter manually")
101+
102+
103+
def get_release_version() -> str:
104+
log = run(
105+
"git",
106+
"log",
107+
"-1",
108+
"--pretty=format:%s",
109+
force_run=True,
110+
)
111+
if match := re.search(r"bump version .* -> ([\d.]+)", log):
112+
return match.group(1)
113+
print("Could not find version in latest commit message")
114+
raise typer.Exit(1)
115+
116+
117+
def update_changelog(new_version: str) -> None:
118+
repo_url = run("git", "remote", "get-url", "origin").strip().replace(".git", "")
119+
changelog = Path("CHANGELOG.md")
120+
content = changelog.read_text()
121+
122+
content = re.sub(
123+
r"## \[Unreleased\]",
124+
f"## [{new_version}]",
125+
content,
126+
count=1,
127+
)
128+
content = re.sub(
129+
rf"## \[{new_version}\]",
130+
f"## [Unreleased]\n\n## [{new_version}]",
131+
content,
132+
count=1,
133+
)
134+
content += f"[{new_version}]: {repo_url}/releases/tag/v{new_version}\n"
135+
content = re.sub(
136+
r"\[unreleased\]: .*\n",
137+
f"[unreleased]: {repo_url}/compare/v{new_version}...HEAD\n",
138+
content,
139+
count=1,
140+
)
141+
142+
changelog.write_text(content)
143+
run("git", "add", ".")
144+
run("git", "commit", "-m", f"update CHANGELOG for version {new_version}")
145+
146+
147+
cli = typer.Typer()
148+
149+
150+
class Version(str, Enum):
151+
MAJOR = "major"
152+
MINOR = "minor"
153+
PATCH = "patch"
154+
155+
156+
class Tag(str, Enum):
157+
DEV = "dev"
158+
ALPHA = "alpha"
159+
BETA = "beta"
160+
RC = "rc"
161+
FINAL = "final"
162+
163+
164+
@cli.command()
165+
def version(
166+
version: Annotated[
167+
Version, Option("--version", "-v", help="The tag to add to the new version")
168+
],
169+
tag: Annotated[Tag, Option("--tag", "-t", help="The tag to add to the new version")]
170+
| None = None,
171+
dry_run: Annotated[
172+
bool, Option("--dry-run", "-d", help="Show commands without executing")
173+
] = False,
174+
):
175+
init_runner(dry_run)
176+
177+
current_version = get_current_version()
178+
changes = run(
179+
"git",
180+
"log",
181+
f"{current_version}..HEAD",
182+
"--pretty=format:- `%h`: %s",
183+
"--reverse",
184+
force_run=True,
185+
)
186+
187+
new_version = get_new_version(version, tag)
188+
release_branch = f"release-v{new_version}"
189+
190+
run("git", "checkout", "-b", release_branch)
191+
run("bumpver", "update", tag=tag, **{version: True})
192+
update_changelog(new_version)
193+
194+
run("git", "push", "--set-upstream", "origin", release_branch)
195+
title = run("git", "log", "-1", "--pretty=%s")
196+
run(
197+
"gh",
198+
"pr",
199+
"create",
200+
"--base",
201+
"main",
202+
"--head",
203+
release_branch,
204+
"--title",
205+
title,
206+
"--body",
207+
changes,
208+
)
209+
210+
211+
@cli.command()
212+
def release(
213+
dry_run: Annotated[
214+
bool, Option("--dry-run", "-d", help="Show commands without executing")
215+
] = False,
216+
force: Annotated[bool, Option("--force", "-f", help="Skip safety checks")] = False,
217+
):
218+
init_runner(dry_run)
219+
220+
current_branch = run("git", "branch", "--show-current", force_run=True).strip()
221+
if current_branch != "main" and not force:
222+
print(
223+
f"Must be on main branch to create release (currently on {current_branch})"
224+
)
225+
raise typer.Exit(1)
226+
227+
if run("git", "status", "--porcelain") and not force:
228+
print("Working directory is not clean. Commit or stash changes first.")
229+
raise typer.Exit(1)
230+
231+
run("git", "fetch", "origin", "main")
232+
local_sha = run("git", "rev-parse", "@").strip()
233+
remote_sha = run("git", "rev-parse", "@{u}").strip()
234+
if local_sha != remote_sha and not force:
235+
print("Local main is not up to date with remote. Pull changes first.")
236+
raise typer.Exit(1)
237+
238+
version = get_release_version()
239+
240+
try:
241+
run("gh", "release", "view", f"v{version}")
242+
if not force:
243+
print(f"Release v{version} already exists!")
244+
raise typer.Exit(1)
245+
except Exception:
246+
pass
247+
248+
if not force and not dry_run:
249+
typer.confirm(f"Create release v{current_version}?", abort=True)
250+
251+
run("gh", "release", "create", f"v{current_version}", "--generate-notes")
252+
253+
254+
if __name__ == "__main__":
255+
cli()

Diff for: .just/project.just

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
set unstable := true
2+
3+
justfile := justfile_directory() + "/.just/project.just"
4+
5+
[private]
6+
default:
7+
@just --list --justfile {{ justfile }}
8+
9+
[private]
10+
fmt:
11+
@just --fmt --justfile {{ justfile }}
12+
13+
[no-cd]
14+
@bump *ARGS:
15+
{{ justfile_directory() }}/.bin/bump.py version {{ ARGS }}
16+
17+
[no-cd]
18+
@release *ARGS:
19+
{{ justfile_directory() }}/.bin/bump.py release {{ ARGS }}

Diff for: CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
1818

1919
## [Unreleased]
2020

21+
## [0.3.0]
22+
2123
### Added
2224

2325
- Added `SyncGitHubAPI`, a synchronous implementation of `gidgethub.abc.GitHubAPI` for Django applications running under WSGI. Maintains the familiar gidgethub interface without requiring async/await.
@@ -58,7 +60,8 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
5860

5961
- Josh Thomas <[email protected]> (maintainer)
6062

61-
[unreleased]: https://github.com/joshuadavidthomas/django-github-app/compare/v0.2.1...HEAD
63+
[unreleased]: https://github.com/joshuadavidthomas/django-github-app/compare/v0.3.0...HEAD
6264
[0.1.0]: https://github.com/joshuadavidthomas/django-github-app/releases/tag/v0.1.0
6365
[0.2.0]: https://github.com/joshuadavidthomas/django-github-app/releases/tag/v0.2.0
6466
[0.2.1]: https://github.com/joshuadavidthomas/django-github-app/releases/tag/v0.2.1
67+
[0.3.0]: https://github.com/joshuadavidthomas/django-github-app/releases/tag/v0.3.0

Diff for: Justfile

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ set dotenv-load := true
22
set unstable := true
33

44
mod docs ".just/documentation.just"
5+
mod project ".just/project.just"
56

67
[private]
78
default:

Diff for: pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Source = "https://github.com/joshuadavidthomas/django-github-app"
7070
[tool.bumpver]
7171
commit = true
7272
commit_message = ":bookmark: bump version {old_version} -> {new_version}"
73-
current_version = "0.2.1"
73+
current_version = "0.3.0"
7474
push = false # set to false for CI
7575
tag = false
7676
version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]"

Diff for: src/django_github_app/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from __future__ import annotations
22

3-
__version__ = "0.2.1"
3+
__version__ = "0.3.0"

Diff for: tests/test_version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55

66
def test_version():
7-
assert __version__ == "0.2.1"
7+
assert __version__ == "0.3.0"

0 commit comments

Comments
 (0)