Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create a GitHub release when publishing #6

Merged
merged 4 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ Bump'X intend to be customizable with the following features:

## Compatibility

Bump'X requires Python `>=3.9` (and `<4.0`)
Bump'X requires Python `>=3.9` (and `<4.0`).

Bump'X also requires that you have [git CLI](https://git-scm.com/) and [GitHub CLI](https://cli.github.com/) installed on your system.

## Installation

Expand Down Expand Up @@ -53,6 +55,7 @@ Here's an exemple:
[bumpx]
file = fake/__init__.py
vcs = git
forge = github
tests = tox
publish = python setup.py sdist register upload
clean =
Expand Down Expand Up @@ -85,6 +88,12 @@ bumpx -M # Bump the major
bumpx # Bump the default part aka. patch
```

If you use GitHub as a forge and publish release on it, you might have to specify which origin should be used to publish the releases, with:

```bash
gh repo set-default
```

## Documentation

The documentation for the upstream project [Bump'X](https://github.com/datagouv/bumpx) is hosted on Read the Docs:
Expand Down
1 change: 1 addition & 0 deletions bumpr.rc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
file = pyproject.toml
regex = version\s*=\s*"(?P<version>.+?)"
vcs = git
forge = github
tag_annotation = version {version}
push = true
tests =
Expand Down
3 changes: 2 additions & 1 deletion bumpx/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"regex": r'(__version__|VERSION)\s*=\s*(\'|")(?P<version>.+?)(\'|")',
"encoding": "utf8",
"vcs": None,
"forge": None,
"commit": True,
"tag": True,
"tag_format": "{version}",
Expand Down Expand Up @@ -155,7 +156,7 @@ def override_from_config(self, filename: str) -> None:
self[hook.key] = False

def override_from_args(self, parsed_args):
for arg in "file", "vcs", "files":
for arg in "file", "vcs", "forge", "files":
if arg in parsed_args and getattr(parsed_args, arg) not in (
None,
[],
Expand Down
34 changes: 34 additions & 0 deletions bumpx/forge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import logging
from typing import Optional

from .helpers import execute

log = logging.getLogger(__name__)


class BaseForge:
def __init__(self, verbose: bool = False) -> None:
self.verbose = verbose

def execute(self, command: list[str]) -> None:
"""Execute a command"""
execute(command, verbose=self.verbose)

def release(self, version: str, notes: Optional[str] = None) -> None:
"""Create a release on the forge"""
raise NotImplementedError


class GitHub(BaseForge):
def release(self, version: str, notes: Optional[str] = "") -> None:
self.execute(["gh", "release", "create", version, "--title", version, "--notes", notes])


class GitLab(BaseForge):
pass


FORGES = {
"github": GitHub,
"gitlab": GitLab,
}
4 changes: 1 addition & 3 deletions bumpx/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ def execute(
if hasattr(exception, "output") and exception.output:
print(exception.output)
cmd = " ".join(cmd) if isinstance(cmd, (list, tuple)) else cmd
raise BumprError(
'Command "{0}" failed with exit code {1}'.format(cmd, exception.returncode)
)
raise BumprError(f'Command "{cmd}" failed with exit code {exception.returncode}')
return output


Expand Down
44 changes: 32 additions & 12 deletions bumpx/releaser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import re
from datetime import datetime
from difflib import unified_diff
from typing import Optional

from .forge import FORGES
from .helpers import BumprError, execute
from .hooks import HOOKS
from .vcs import VCS
Expand All @@ -25,30 +27,33 @@ def __init__(self, config):
version_string = match.group("version")
self.prev_version = Version.parse(version_string)
except Exception:
raise BumprError("Unable to extract version from {0}".format(config.file))
raise BumprError(f"Unable to extract version from {config.file}")

logger.debug("Previous version: {0}".format(self.prev_version))
logger.debug(f"Previous version: {self.prev_version}")

self.version = self.prev_version.copy()
self.version.bump(config.bump.part, config.bump.unsuffix, config.bump.suffix)
logger.debug("Bumped version: {0}".format(self.version))
logger.debug(f"Bumped version: {self.version}")

self.next_version = self.version.copy()
self.next_version.bump(config.prepare.part, config.prepare.unsuffix, config.prepare.suffix)
logger.debug("Prepared version: {0}".format(self.next_version))
logger.debug(f"Prepared version: {self.next_version}")

self.tag_label = self.config.tag_format.format(version=self.version)
logger.debug("Tag: {0}".format(self.tag_label))
logger.debug(f"Tag: {self.tag_label}")
if self.config.tag_annotation:
self.tag_annotation = self.config.tag_annotation.format(version=self.version)
logger.debug("Tag annotation: {0}".format(self.tag_annotation))
logger.debug(f"Tag annotation: {self.tag_annotation}")

self.timestamp = None

if config.vcs:
self.vcs = VCS[config.vcs](verbose=config.verbose)
self.vcs.validate(dryrun=config.dryrun)

if config.forge:
self.forge = FORGES[config.forge](verbose=config.verbose)

if config.dryrun:
self.modified = {}
self.diffs = {}
Expand Down Expand Up @@ -170,35 +175,50 @@ def bump_files(self, replacements):
self.perform(filename, before, after)

def publish(self):
"""Publish the current release to PyPI"""
"""Publish the current release to PyPI and to the Forge if defined"""
if self.config.publish:
logger.info("Publish")
self.execute(self.config.publish)

if self.config.forge:
self.create_forge_release() # TODO: get the release notes from the changelog hook

def tag(self):
if self.config.commit and self.config.tag:
if self.config.tag_annotation:
logger.debug(f"Tag: {self.tag_label} Annotation: {self.tag_annotation}")
if not self.config.dryrun:
self.vcs.tag(self.tag_label, self.tag_annotation)
else:
logger.dryrun(
"tag: {0} annotation: {1}".format(self.tag_label, self.tag_annotation)
)
logger.dryrun(f"tag: {self.tag_label} annotation: {self.tag_annotation}")
else:
logger.debug(f"Tag: {self.tag_label}")
if not self.config.dryrun:
self.vcs.tag(self.tag_label)
else:
logger.dryrun("tag: {0}".format(self.tag_label))
logger.dryrun(f"tag: {self.tag_label}")

def create_forge_release(self, notes: Optional[str] = None) -> None:
if self.config.tag:
if notes:
logger.debug(f"Forge release: {self.tag_label} with notes")
else:
logger.debug(f"Forge release: {self.tag_label}")
if not self.config.dryrun:
self.forge.release(version=self.tag_label, notes=notes)
else:
if notes:
logger.dryrun(f"Forge release: {self.tag_label} with notes") # type: ignore
else:
logger.dryrun(f"Forge release: {self.tag_label}") # type: ignore

def commit(self, message):
if self.config.commit:
logger.debug(f"Commit: {message}")
if not self.config.dryrun:
self.vcs.commit(message)
else:
logger.dryrun("commit: {0}".format(message))
logger.dryrun(f"commit: {message}")

def push(self):
if self.config.vcs and self.config.commit and self.config.push:
Expand Down
33 changes: 33 additions & 0 deletions tests/test_forges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from bumpx.forge import BaseForge, GitHub


class BaseForgeTest:
def test_execute_verbose(self, mocker):
forge = BaseForge(verbose=True)
execute = mocker.patch("bumpx.forge.execute")
forge.execute("cmd arg")
execute.assert_called_with("cmd arg", verbose=True)

def test_execute_quiet(self, mocker):
Pierlou marked this conversation as resolved.
Show resolved Hide resolved
forge = BaseForge(verbose=False)
execute = mocker.patch("bumpx.forge.execute")
forge.execute("cmd arg")
execute.assert_called_with("cmd arg", verbose=False)


class GitHubTest:
def test_release(self, mocker):
github = GitHub()

execute = mocker.patch.object(github, "execute")
github.release(version="fake")
execute.assert_called_with(["gh", "release", "create", "fake", "--title", "fake"])

def test_release_with_notes(self, mocker):
github = GitHub()

execute = mocker.patch.object(github, "execute")
github.release(version="fake", notes="some notes")
execute.assert_called_with(
["gh", "release", "create", "fake", "--title", "fake", "--notes", "some notes"]
)
Loading