Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

feat(bump): added support for running arbitrary hooks during bump #644

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

Merged
merged 3 commits into from
Feb 8, 2023
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
2 changes: 1 addition & 1 deletion .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jobs:
python-check:
strategy:
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
python-version: ["3.7", "3.8", "3.9", "3.10"]
platform: [ubuntu-20.04, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
Expand Down
3 changes: 2 additions & 1 deletion commitizen/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ def _try_decode(bytes_: bytes) -> str:
raise CharacterSetDecodeError() from e


def run(cmd: str) -> Command:
def run(cmd: str, env=None) -> Command:
process = subprocess.Popen(
cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
env=env,
)
stdout, stderr = process.communicate()
return_code = process.returncode
Expand Down
32 changes: 31 additions & 1 deletion commitizen/commands/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import questionary
from packaging.version import InvalidVersion, Version

from commitizen import bump, cmd, defaults, factory, git, out
from commitizen import bump, cmd, defaults, factory, git, hooks, out
from commitizen.commands.changelog import Changelog
from commitizen.config import BaseConfig
from commitizen.exceptions import (
Expand Down Expand Up @@ -58,6 +58,8 @@ def __init__(self, config: BaseConfig, arguments: dict):
self.no_verify = arguments["no_verify"]
self.check_consistency = arguments["check_consistency"]
self.retry = arguments["retry"]
self.pre_bump_hooks = self.config.settings["pre_bump_hooks"]
self.post_bump_hooks = self.config.settings["post_bump_hooks"]

def is_initial_tag(self, current_tag_version: str, is_yes: bool = False) -> bool:
"""Check if reading the whole git tree up to HEAD is needed."""
Expand Down Expand Up @@ -272,6 +274,20 @@ def __call__(self): # noqa: C901

self.config.set_key("version", str(new_version))

if self.pre_bump_hooks:
hooks.run(
self.pre_bump_hooks,
_env_prefix="CZ_PRE_",
is_initial=is_initial,
current_version=current_version,
current_tag_version=current_tag_version,
new_version=new_version.public,
new_tag_version=new_tag_version,
message=message,
increment=increment,
changelog_file_name=changelog_cmd.file_name if self.changelog else None,
)

if is_files_only:
raise ExpectedExit()

Expand Down Expand Up @@ -300,6 +316,20 @@ def __call__(self): # noqa: C901
if c.return_code != 0:
raise BumpTagFailedError(c.err)

if self.post_bump_hooks:
hooks.run(
self.post_bump_hooks,
_env_prefix="CZ_POST_",
was_initial=is_initial,
previous_version=current_version,
previous_tag_version=current_tag_version,
current_version=new_version.public,
current_tag_version=new_tag_version,
message=message,
increment=increment,
changelog_file_name=changelog_cmd.file_name if self.changelog else None,
)

# TODO: For v3 output this only as diagnostic and remove this if
if self.changelog_to_stdout:
out.diagnostic("Done!")
Expand Down
4 changes: 4 additions & 0 deletions commitizen/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class Settings(TypedDict, total=False):
style: Optional[List[Tuple[str, str]]]
customize: CzSettings
major_version_zero: bool
pre_bump_hooks: Optional[List[str]]
post_bump_hooks: Optional[List[str]]


name: str = "cz_conventional_commits"
Expand All @@ -65,6 +67,8 @@ class Settings(TypedDict, total=False):
"update_changelog_on_bump": False,
"use_shortcuts": False,
"major_version_zero": False,
"pre_bump_hooks": [],
"post_bump_hooks": [],
}

MAJOR = "MAJOR"
Expand Down
5 changes: 5 additions & 0 deletions commitizen/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class ExitCode(enum.IntEnum):
GIT_COMMAND_ERROR = 23
INVALID_MANUAL_VERSION = 24
INIT_FAILED = 25
RUN_HOOK_FAILED = 26


class CommitizenException(Exception):
Expand Down Expand Up @@ -168,3 +169,7 @@ class InvalidManualVersion(CommitizenException):

class InitFailedError(CommitizenException):
exit_code = ExitCode.INIT_FAILED


class RunHookError(CommitizenException):
exit_code = ExitCode.RUN_HOOK_FAILED
34 changes: 34 additions & 0 deletions commitizen/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import annotations

from commitizen import cmd, out
from commitizen.exceptions import RunHookError


def run(hooks, _env_prefix="CZ_", **env):
if isinstance(hooks, str):
hooks = [hooks]

for hook in hooks:
out.info(f"Running hook '{hook}'")

c = cmd.run(hook, env=_format_env(_env_prefix, env))

if c.out:
out.write(c.out)
if c.err:
out.error(c.err)

if c.return_code != 0:
raise RunHookError(f"Running hook '{hook}' failed")


def _format_env(prefix: str, env: dict[str, str]) -> dict[str, str]:
"""_format_env() prefixes all given environment variables with the given
prefix so it can be passed directly to cmd.run()."""
penv = dict()
for name, value in env.items():
name = prefix + name.upper()
value = str(value) if value is not None else ""
penv[name] = value

return penv
55 changes: 55 additions & 0 deletions docs/bump.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,61 @@ Defaults to: `false`
major_version_zero = true
```

---

### `pre_bump_hooks`

A list of optional commands that will run right _after_ updating `version_files`
and _before_ actual committing and tagging the release.

Useful when you need to generate documentation based on the new version. During
execution of the script, some environment variables are available:

| Variable | Description |
| ---------------------------- | ---------------------------------------------------------- |
| `CZ_PRE_IS_INITIAL` | `True` when this is the initial release, `False` otherwise |
| `CZ_PRE_CURRENT_VERSION` | Current version, before the bump |
| `CZ_PRE_CURRENT_TAG_VERSION` | Current version tag, before the bump |
| `CZ_PRE_NEW_VERSION` | New version, after the bump |
| `CZ_PRE_NEW_TAG_VERSION` | New version tag, after the bump |
| `CZ_PRE_MESSAGE` | Commit message of the bump |
| `CZ_PRE_INCREMENT` | Whether this is a `MAJOR`, `MINOR` or `PATH` release |
| `CZ_PRE_CHANGELOG_FILE_NAME` | Path to the changelog file, if available |

```toml
[tool.commitizen]
pre_bump_hooks = [
"scripts/generate_documentation.sh"
]
```

---

### `post_bump_hooks`

A list of optional commands that will run right _after_ committing and tagging the release.

Useful when you need to send notifications about a release, or further automate deploying the
release. During execution of the script, some environment variables are available:

| Variable | Description |
| ------------------------------ | ----------------------------------------------------------- |
| `CZ_POST_WAS_INITIAL` | `True` when this was the initial release, `False` otherwise |
| `CZ_POST_PREVIOUS_VERSION` | Previous version, before the bump |
| `CZ_POST_PREVIOUS_TAG_VERSION` | Previous version tag, before the bump |
| `CZ_POST_CURRENT_VERSION` | Current version, after the bump |
| `CZ_POST_CURRENT_TAG_VERSION` | Current version tag, after the bump |
| `CZ_POST_MESSAGE` | Commit message of the bump |
| `CZ_POST_INCREMENT` | Whether this wass a `MAJOR`, `MINOR` or `PATH` release |
| `CZ_POST_CHANGELOG_FILE_NAME` | Path to the changelog file, if available |

```toml
[tool.commitizen]
post_bump_hooks = [
"scripts/slack_notification.sh"
]
```

## Custom bump

Read the [customizing section](./customization.md).
Expand Down
59 changes: 57 additions & 2 deletions tests/commands/test_bump_command.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import inspect
import sys
from typing import Tuple
from unittest.mock import MagicMock
from unittest.mock import MagicMock, call

import pytest
from pytest_mock import MockFixture

import commitizen.commands.bump as bump
from commitizen import cli, cmd, git
from commitizen import cli, cmd, git, hooks
from commitizen.exceptions import (
BumpTagFailedError,
CommitizenException,
Expand Down Expand Up @@ -759,3 +759,58 @@ def test_bump_manual_version_disallows_major_version_zero(mocker):
"--major-version-zero cannot be combined with MANUAL_VERSION"
)
assert expected_error_message in str(excinfo.value)


@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file"))
def test_bump_with_pre_bump_hooks(
commit_msg, mocker: MockFixture, tmp_commitizen_project
):
pre_bump_hook = "scripts/pre_bump_hook.sh"
post_bump_hook = "scripts/post_bump_hook.sh"

tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml")
tmp_commitizen_cfg_file.write(
f"{tmp_commitizen_cfg_file.read()}\n"
f'pre_bump_hooks = ["{pre_bump_hook}"]\n'
f'post_bump_hooks = ["{post_bump_hook}"]\n'
)

run_mock = mocker.Mock()
mocker.patch.object(hooks, "run", run_mock)

create_file_and_commit(commit_msg)
testargs = ["cz", "bump", "--yes"]
mocker.patch.object(sys, "argv", testargs)
cli.main()

tag_exists = git.tag_exist("0.2.0")
assert tag_exists is True

run_mock.assert_has_calls(
[
call(
[pre_bump_hook],
_env_prefix="CZ_PRE_",
is_initial=True,
current_version="0.1.0",
current_tag_version="0.1.0",
new_version="0.2.0",
new_tag_version="0.2.0",
message="bump: version 0.1.0 → 0.2.0",
increment="MINOR",
changelog_file_name=None,
),
call(
[post_bump_hook],
_env_prefix="CZ_POST_",
was_initial=True,
previous_version="0.1.0",
previous_tag_version="0.1.0",
current_version="0.2.0",
current_tag_version="0.2.0",
message="bump: version 0.1.0 → 0.2.0",
increment="MINOR",
changelog_file_name=None,
),
]
)
38 changes: 38 additions & 0 deletions tests/test_bump_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from unittest.mock import call

import pytest
from pytest_mock import MockFixture

from commitizen import cmd, hooks
from commitizen.exceptions import RunHookError


def test_run(mocker: MockFixture):
bump_hooks = ["pre_bump_hook", "pre_bump_hook_1"]

cmd_run_mock = mocker.Mock()
cmd_run_mock.return_value.return_code = 0
mocker.patch.object(cmd, "run", cmd_run_mock)

hooks.run(bump_hooks)

cmd_run_mock.assert_has_calls(
[call("pre_bump_hook", env={}), call("pre_bump_hook_1", env={})]
)


def test_run_error(mocker: MockFixture):
bump_hooks = ["pre_bump_hook", "pre_bump_hook_1"]

cmd_run_mock = mocker.Mock()
cmd_run_mock.return_value.return_code = 1
mocker.patch.object(cmd, "run", cmd_run_mock)

with pytest.raises(RunHookError):
hooks.run(bump_hooks)


def test_format_env():
result = hooks._format_env("TEST_", {"foo": "bar", "bar": "baz"})
assert "TEST_FOO" in result and result["TEST_FOO"] == "bar"
assert "TEST_BAR" in result and result["TEST_BAR"] == "baz"
12 changes: 12 additions & 0 deletions tests/test_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
["pointer", "reverse"],
["question", "underline"]
]
pre_bump_hooks = [
"scripts/generate_documentation.sh"
]
post_bump_hooks = ["scripts/slack_notification.sh"]

[tool.black]
line-length = 88
Expand All @@ -31,6 +35,8 @@
"version": "1.0.0",
"version_files": ["commitizen/__version__.py", "pyproject.toml"],
"style": [["pointer", "reverse"], ["question", "underline"]],
"pre_bump_hooks": ["scripts/generate_documentation.sh"],
"post_bump_hooks": ["scripts/slack_notification.sh"],
}
}

Expand All @@ -49,6 +55,8 @@
"update_changelog_on_bump": False,
"use_shortcuts": False,
"major_version_zero": False,
"pre_bump_hooks": ["scripts/generate_documentation.sh"],
"post_bump_hooks": ["scripts/slack_notification.sh"],
}

_new_settings = {
Expand All @@ -65,6 +73,8 @@
"update_changelog_on_bump": False,
"use_shortcuts": False,
"major_version_zero": False,
"pre_bump_hooks": ["scripts/generate_documentation.sh"],
"post_bump_hooks": ["scripts/slack_notification.sh"],
}

_read_settings = {
Expand All @@ -73,6 +83,8 @@
"version_files": ["commitizen/__version__.py", "pyproject.toml"],
"style": [["pointer", "reverse"], ["question", "underline"]],
"changelog_file": "CHANGELOG.md",
"pre_bump_hooks": ["scripts/generate_documentation.sh"],
"post_bump_hooks": ["scripts/slack_notification.sh"],
}


Expand Down