Skip to content

Commit

Permalink
feat: include additional changes in release commits
Browse files Browse the repository at this point in the history
Add new config keys, `pre_commit_command` and `commit_additional_files`,
to allow custom file changes alongside the release commits.
  • Loading branch information
MattPColeman authored and relekang committed Jan 24, 2022
1 parent 09af5f1 commit 3e34f95
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 1 deletion.
13 changes: 13 additions & 0 deletions docs/configuration.rst
Expand Up @@ -121,6 +121,19 @@ set this option to `false`.

Default: `true`.

.. _config-pre_commit_command:

``pre_commit_command``
----------------------
If this command is provided, it will be run prior to the creation of the release commit.

.. _config-include_additional_files:

``include_additional_files``
----------------------------
A comma-separated list of files to be included within the release commit. This can include
any files created/modified by the ``pre_commit_command``.

Commit Parsing
==============

Expand Down
7 changes: 7 additions & 0 deletions semantic_release/cli.py
Expand Up @@ -29,6 +29,7 @@
post_changelog,
upload_to_release,
)
from .pre_commit import run_pre_commit, should_run_pre_commit
from .repository import ArtifactRepo
from .settings import config, overload_configuration
from .vcs_helpers import (
Expand All @@ -38,6 +39,7 @@
get_repository_owner_and_name,
push_new_version,
tag_new_version,
update_additional_files,
update_changelog_file,
)

Expand Down Expand Up @@ -272,8 +274,13 @@ def publish(retry: bool = False, noop: bool = False, **kwargs):
previous_version=current_version,
)

if should_run_pre_commit():
logger.info("Running pre-commit command")
run_pre_commit()

if not retry:
update_changelog_file(new_version, changelog_md)
update_additional_files()
bump_version(new_version, level_bump)
# A new version was released
logger.info("Pushing new version")
Expand Down
2 changes: 2 additions & 0 deletions semantic_release/defaults.cfg
Expand Up @@ -12,6 +12,7 @@ changelog_placeholder=<!--next-version-placeholder-->
changelog_scope=true
changelog_sections=feature,fix,breaking,documentation,performance,:boom:,:sparkles:,:children_crossing:,:lipstick:,:iphone:,:egg:,:chart_with_upwards_trend:,:ambulance:,:lock:,:bug:,:zap:,:goal_net:,:alien:,:wheelchair:,:speech_balloon:,:mag:,:apple:,:penguin:,:checkered_flag:,:robot:,:green_apple:,Other
check_build_status=false
include_additional_files=
commit_message=Automatically generated by python-semantic-release
commit_parser=semantic_release.history.angular_parser
commit_subject={version}
Expand All @@ -26,6 +27,7 @@ minor_emoji=:sparkles:,:children_crossing:,:lipstick:,:iphone:,:egg:,:chart_with
minor_tag=:sparkles:
patch_emoji=:ambulance:,:lock:,:bug:,:zap:,:goal_net:,:alien:,:wheelchair:,:speech_balloon:,:mag:,:apple:,:penguin:,:checkered_flag:,:robot:,:green_apple:
patch_without_tag=false
pre_commit_command=
pypi_pass_var=PYPI_PASSWORD
pypi_token_var=PYPI_TOKEN
pypi_user_var=PYPI_USERNAME
Expand Down
20 changes: 20 additions & 0 deletions semantic_release/pre_commit.py
@@ -0,0 +1,20 @@
"""Run commands prior to the release commit
"""
import logging

from invoke import run

from .settings import config

logger = logging.getLogger(__name__)


def should_run_pre_commit():
command = config.get("pre_commit_command")
return bool(command)


def run_pre_commit():
command = config.get("pre_commit_command")
logger.debug(f"Running {command}")
run(command)
32 changes: 31 additions & 1 deletion semantic_release/vcs_helpers.py
Expand Up @@ -6,7 +6,7 @@
from datetime import date
from functools import wraps
from pathlib import Path, PurePath
from typing import Optional, Tuple
from typing import List, Optional, Tuple
from urllib.parse import urlsplit

from git import GitCommandError, InvalidGitRepositoryError, Repo
Expand Down Expand Up @@ -210,6 +210,36 @@ def update_changelog_file(version: str, content_to_add: str):
repo.git.add(str(git_path.relative_to(str(repo.working_dir))))


def get_changed_files(repo: Repo) -> List[str]:
"""
Get untracked / dirty files in the given git repo.
:param repo: Git repo to check.
:return: A list of filenames.
"""
untracked_files = repo.untracked_files
dirty_files = [item.a_path for item in repo.index.diff(None)]
return [*untracked_files, *dirty_files]


@check_repo
@LoggedFunction(logger)
def update_additional_files():
"""
Add specified files to VCS, if they've changed.
"""
changed_files = get_changed_files(repo)

include_additional_files = config.get("include_additional_files")
if include_additional_files:
for filename in include_additional_files.split(","):
if filename in changed_files:
logger.debug(f"Updated file: {filename}")
repo.git.add(filename)
else:
logger.warning(f"File {filename} shows no changes, cannot update it.")


@check_repo
@LoggedFunction(logger)
def tag_new_version(version: str):
Expand Down
89 changes: 89 additions & 0 deletions tests/test_cli.py
Expand Up @@ -452,6 +452,95 @@ def test_version_retry(mocker):
mock_get_new.assert_called_once_with("current", "patch")


def test_publish_should_not_run_pre_commit_by_default(mocker):
mocker.patch("semantic_release.cli.checkout")
mocker.patch("semantic_release.cli.ci_checks.check")
mocker.patch.object(ArtifactRepo, "upload")
mocker.patch("semantic_release.cli.upload_to_release")
mocker.patch("semantic_release.cli.post_changelog", lambda *x: True)
mocker.patch("semantic_release.cli.push_new_version", return_value=True)
mocker.patch("semantic_release.cli.should_bump_version", return_value=True)
mocker.patch("semantic_release.cli.markdown_changelog", lambda *x, **y: "CHANGES")
mocker.patch("semantic_release.cli.update_changelog_file")
mocker.patch("semantic_release.cli.bump_version")
mocker.patch("semantic_release.cli.get_new_version", lambda *x: "2.0.0")
mocker.patch("semantic_release.cli.check_token", lambda: True)
run_pre_commit = mocker.patch("semantic_release.cli.run_pre_commit")
mocker.patch(
"semantic_release.cli.config.get",
wrapped_config_get(
remove_dist=False,
upload_to_pypi=False,
upload_to_release=False,
),
)
mocker.patch("semantic_release.cli.update_changelog_file", lambda *x, **y: None)

publish()

assert not run_pre_commit.called


def test_publish_should_not_run_pre_commit_with_empty_command(mocker):
mocker.patch("semantic_release.cli.checkout")
mocker.patch("semantic_release.cli.ci_checks.check")
mocker.patch.object(ArtifactRepo, "upload")
mocker.patch("semantic_release.cli.upload_to_release")
mocker.patch("semantic_release.cli.post_changelog", lambda *x: True)
mocker.patch("semantic_release.cli.push_new_version", return_value=True)
mocker.patch("semantic_release.cli.should_bump_version", return_value=True)
mocker.patch("semantic_release.cli.markdown_changelog", lambda *x, **y: "CHANGES")
mocker.patch("semantic_release.cli.update_changelog_file")
mocker.patch("semantic_release.cli.bump_version")
mocker.patch("semantic_release.cli.get_new_version", lambda *x: "2.0.0")
mocker.patch("semantic_release.cli.check_token", lambda: True)
run_pre_commit = mocker.patch("semantic_release.cli.run_pre_commit")
mocker.patch(
"semantic_release.cli.config.get",
wrapped_config_get(
remove_dist=False,
upload_to_pypi=False,
upload_to_release=False,
pre_commit_command="",
),
)
mocker.patch("semantic_release.cli.update_changelog_file", lambda *x, **y: None)

publish()

assert not run_pre_commit.called


def test_publish_should_run_pre_commit_if_provided(mocker):
mocker.patch("semantic_release.cli.checkout")
mocker.patch("semantic_release.cli.ci_checks.check")
mocker.patch.object(ArtifactRepo, "upload")
mocker.patch("semantic_release.cli.upload_to_release")
mocker.patch("semantic_release.cli.post_changelog", lambda *x: True)
mocker.patch("semantic_release.cli.push_new_version", return_value=True)
mocker.patch("semantic_release.cli.should_bump_version", return_value=True)
mocker.patch("semantic_release.cli.markdown_changelog", lambda *x, **y: "CHANGES")
mocker.patch("semantic_release.cli.update_changelog_file")
mocker.patch("semantic_release.cli.bump_version")
mocker.patch("semantic_release.cli.get_new_version", lambda *x: "2.0.0")
mocker.patch("semantic_release.cli.check_token", lambda: True)
run_pre_commit = mocker.patch("semantic_release.cli.run_pre_commit")
mocker.patch(
"semantic_release.cli.config.get",
wrapped_config_get(
remove_dist=False,
upload_to_pypi=False,
upload_to_release=False,
pre_commit_command="echo \"Hello, world.\"",
),
)
mocker.patch("semantic_release.cli.update_changelog_file", lambda *x, **y: None)

publish()

assert run_pre_commit.called


def test_publish_should_not_upload_to_pypi_if_option_is_false(mocker):
mocker.patch("semantic_release.cli.checkout")
mocker.patch("semantic_release.cli.ci_checks.check")
Expand Down
46 changes: 46 additions & 0 deletions tests/test_pre_commit.py
@@ -0,0 +1,46 @@
from semantic_release.pre_commit import run_pre_commit, should_run_pre_commit

import pytest


@pytest.mark.parametrize(
"commands",
["make do-stuff", "echo hello > somefile.txt"],
)
def test_pre_commit_command(mocker, commands):
mocker.patch("semantic_release.pre_commit.config.get", lambda *a: commands)
mock_run = mocker.patch("semantic_release.pre_commit.run")
run_pre_commit()
mock_run.assert_called_once_with(commands)


@pytest.mark.parametrize(
"config,expected",
[
(
{
"pre_commit_command": "make generate_some_file",
},
True,
),
(
{
"pre_commit_command": "cmd",
},
True,
),
(
{
"pre_commit_command": "",
},
False,
),
(
{},
False,
),
],
)
def test_should_run_pre_commit_command(config, expected, mocker):
mocker.patch("semantic_release.cli.config.get", lambda key: config.get(key))
assert should_run_pre_commit() == expected
81 changes: 81 additions & 0 deletions tests/test_vcs_helpers.py
Expand Up @@ -18,6 +18,7 @@
push_new_version,
tag_new_version,
update_changelog_file,
update_additional_files,
)

from . import mock, wrapped_config_get
Expand Down Expand Up @@ -446,3 +447,83 @@ def test_update_changelog_file_missing_placeholder(mock_git, mocker):
mock_git.add.assert_not_called()
mocked_read_text.assert_called_once()
mocked_write_text.assert_not_called()


@pytest.mark.parametrize(
"include_additional_files",
[
"",
",",
"somefile.txt",
"somefile.txt,anotherfile.rst",
"somefile.txt,anotherfile.rst,finalfile.md",
],
)
def test_update_additional_files_with_no_changes(
mock_git,
mocker,
include_additional_files,
):
"""
Since we have no file changes, we expect `add` to never be called,
regardless of the config.
"""
mocker.patch(
"semantic_release.vcs_helpers.config.get",
wrapped_config_get(**{"include_additional_files": include_additional_files}),
)
mocker.patch("semantic_release.vcs_helpers.get_changed_files", return_value=[])
update_additional_files()
mock_git.add.assert_not_called()


def test_update_additional_files_single_changed_file(mock_git, mocker):
"""
We expect to add the single file corresponding to config & changes.
"""
mocker.patch(
"semantic_release.vcs_helpers.config.get",
wrapped_config_get(**{"include_additional_files": "somefile.txt"}),
)
mocker.patch(
"semantic_release.vcs_helpers.get_changed_files",
return_value=["somefile.txt"],
)
update_additional_files()
mock_git.add.assert_called_once_with("somefile.txt")


def test_update_additional_files_one_in_config_two_changes(mock_git, mocker):
"""
Given two file changes, but only one referenced in the config, we
expect that single file to be added.
"""
mocker.patch(
"semantic_release.vcs_helpers.config.get",
wrapped_config_get(**{"include_additional_files": "anotherfile.txt"}),
)
mocker.patch(
"semantic_release.vcs_helpers.get_changed_files",
return_value=["somefile.txt", "anotherfile.txt"],
)
update_additional_files()
mock_git.add.assert_called_once_with("anotherfile.txt")


def test_update_additional_files_two_in_config_one_change(mock_git, mocker):
"""
Given two file changes, but only one referenced in the config, we
expect that single file to be added.
"""
mocker.patch(
"semantic_release.vcs_helpers.config.get",
wrapped_config_get(
**{"include_additional_files": "somefile.txt,anotherfile.txt"}
),
)
mocker.patch(
"semantic_release.vcs_helpers.get_changed_files",
return_value=["anotherfile.txt"],
)
update_additional_files()
mock_git.add.assert_called_once_with("anotherfile.txt")

0 comments on commit 3e34f95

Please sign in to comment.