Skip to content

Commit

Permalink
feat(cmd-version): add --tag/--no-tag option to version command (#752)
Browse files Browse the repository at this point in the history
* fix(version): separate push tags from commit push when not committing changes

* feat(version): add `--no-tag` option to turn off tag creation

* test(version): add test for `--tag` option & `--no-tag/commit`

* docs(commands): update `version` subcommand options
  • Loading branch information
codejedi365 committed Dec 7, 2023
1 parent 744ff25 commit de6b9ad
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 20 deletions.
21 changes: 17 additions & 4 deletions docs/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,26 @@ For example, assuming a project is currently at version 1.2.3::
Whether or not to perform a ``git commit`` on modifications to source files made by ``semantic-release`` during this
command invocation, and to run ``git tag`` on this new commit with a tag corresponding to the new version.

If ``--no-commit`` is supplied, a number of other options are also disabled; please see below.
If ``--no-commit`` is supplied, it may disable other options derivatively; please see below.

**Default:** ``--commit``

.. seealso::
- :ref:`tag_format <config-tag-format>`

.. _cmd-version-option-tag:

``--tag/--no-tag``
************************

Whether or not to perform a ``git tag`` to apply a tag of the corresponding to the new version during this
command invocation. This option manages the tag application separate from the commit handled by the `--commit`
option.

If ``--no-tag`` is supplied, it may disable other options derivatively; please see below.

**Default:** ``--tag``

.. _cmd-version-option-changelog:

``--changelog/--no-changelog``
Expand All @@ -239,10 +252,10 @@ version released.
``--push/--no-push``
********************

Whether or not to push new commits and tags to the remote repository.
Whether or not to push new commits and/or tags to the remote repository.

**Default:** ``--no-push`` if :ref:`--no-commit <cmd-version-option-commit>` is also
supplied, otherwise ``--push``
**Default:** ``--no-push`` if :ref:`--no-commit <cmd-version-option-commit>` and
:ref:`--no-tag <cmd-version-option-tag>` is also supplied, otherwise ``push`` is the default.

.. _cmd-version-option-vcs-release:

Expand Down
56 changes: 40 additions & 16 deletions semantic_release/cli/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ def shell(cmd: str, *, check: bool = True) -> subprocess.CompletedProcess:
default=True,
help="Whether or not to commit changes locally",
)
@click.option(
"--tag/--no-tag",
"create_tag",
default=True,
help="Whether or not to create a tag for the new version"
)
@click.option(
"--changelog/--no-changelog",
"update_changelog",
Expand Down Expand Up @@ -188,6 +194,7 @@ def version( # noqa: C901
prerelease_token: str | None = None,
force_level: str | None = None,
commit_changes: bool = True,
create_tag: bool = True,
update_changelog: bool = True,
push_changes: bool = True,
make_vcs_release: bool = True,
Expand Down Expand Up @@ -237,8 +244,14 @@ def version( # noqa: C901

# Only push if we're committing changes
if push_changes and not commit_changes:
log.info("changes will not be pushed because --no-commit disables pushing")
push_changes &= commit_changes
if not create_tag:
log.info("changes will not be pushed because --no-commit disables pushing")
push_changes &= commit_changes
# Only push if we're creating a tag
if push_changes and not create_tag:
if not commit_changes:
log.info("new tag will not be pushed because --no-tag disables pushing")
push_changes &= create_tag
# Only make a release if we're pushing the changes
if make_vcs_release and not push_changes:
log.info("No vcs release will be created because pushing changes is disabled")
Expand Down Expand Up @@ -484,36 +497,47 @@ def custom_git_environment() -> ContextManager[None]:
# are disabled, and the changelog generation is disabled or it's not
# modified, then the HEAD commit will be tagged as a release commit
# despite not being made by PSR
if commit_changes and opts.noop:
noop_report(
indented(
f"""
would have run:
git tag -a {new_version.as_tag()} -m "{new_version.as_tag()}"
"""
if commit_changes or create_tag:
if opts.noop:
noop_report(
indented(
f"""
would have run:
git tag -a {new_version.as_tag()} -m "{new_version.as_tag()}"
"""
)
)
)
elif commit_changes:
with custom_git_environment():
repo.git.tag("-a", new_version.as_tag(), m=new_version.as_tag())
else:
with custom_git_environment():
repo.git.tag("-a", new_version.as_tag(), m=new_version.as_tag())

if push_changes:
remote_url = runtime.hvcs_client.remote_url(
use_token=not runtime.ignore_token_for_push
)
active_branch = repo.active_branch.name
if opts.noop:
if commit_changes and opts.noop:
noop_report(
indented(
f"""
would have run:
git push {runtime.masker.mask(remote_url)} {active_branch}
git push --tags {runtime.masker.mask(remote_url)} {active_branch}
""" # noqa: E501
)
)
else:
elif commit_changes:
repo.git.push(remote_url, active_branch)

if create_tag and opts.noop:
noop_report(
indented(
f"""
would have run:
git push --tags {runtime.masker.mask(remote_url)} {active_branch}
""" # noqa: E501
)
)
elif create_tag:
repo.git.push("--tags", remote_url, active_branch)

gha_output.released = True
Expand Down
107 changes: 107 additions & 0 deletions tests/command_line/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import filecmp
import re
import shutil
from pathlib import Path
from subprocess import CompletedProcess
from typing import TYPE_CHECKING
from unittest import mock
Expand Down Expand Up @@ -590,3 +591,109 @@ def test_custom_release_notes_template(
)
assert post_mocker.call_count == 1
assert post_mocker.last_request.json()["body"] == expected_release_notes


def test_version_tag_only_push(
mocked_git_push: MagicMock,
runtime_context_with_no_tags: RuntimeContext,
cli_runner: CliRunner,
) -> None:
# Arrange
# (see fixtures)
head_before = runtime_context_with_no_tags.repo.head.commit

# Act
args = [version.name, "--tag", "--no-commit", "--skip-build", "--no-vcs-release"]
resp = cli_runner.invoke(main, args)

tag_after = runtime_context_with_no_tags.repo.tags[-1].name
head_after = runtime_context_with_no_tags.repo.head.commit

# Assert
assert tag_after == "v0.1.0"
assert head_before == head_after
assert mocked_git_push.call_count == 1 # 0 for commit, 1 for tag
assert resp.exit_code == 0, (
"Unexpected failure in command "
f"'semantic-release {str.join(' ', args)}': " + resp.stderr
)


def test_version_only_update_files_no_git_actions(
mocked_git_push: MagicMock,
runtime_context_with_tags: RuntimeContext,
cli_runner: CliRunner,
tmp_path_factory: pytest.TempPathFactory,
example_pyproject_toml: Path
) -> None:
# Arrange
expected_new_version = "0.3.0"
tempdir = tmp_path_factory.mktemp("test_version")
shutil.rmtree(str(tempdir.resolve()))
example_project = Path(runtime_context_with_tags.repo.git.rev_parse("--show-toplevel"))
shutil.copytree(src=str(example_project.resolve()), dst=tempdir)

head_before = runtime_context_with_tags.repo.head.commit
tags_before = runtime_context_with_tags.repo.tags

# Act
args = [version.name, "--minor", "--no-tag", "--no-commit", "--skip-build"]
resp = cli_runner.invoke(main, args)

tags_after = runtime_context_with_tags.repo.tags
head_after = runtime_context_with_tags.repo.head.commit

# Assert
assert tags_before == tags_after
assert head_before == head_after
assert mocked_git_push.call_count == 0 # no push as it should be turned off automatically
assert resp.exit_code == 0, (
"Unexpected failure in command "
f"'semantic-release {str.join(' ', args)}': " + resp.stderr
)

dcmp = filecmp.dircmp(str(example_project.resolve()), tempdir)
differing_files = flatten_dircmp(dcmp)

# Files that should receive version change
assert differing_files == [
"pyproject.toml",
f"src/{EXAMPLE_PROJECT_NAME}/__init__.py",
]

# Compare pyproject.toml
new_pyproject_toml = tomlkit.loads(
example_pyproject_toml.read_text(encoding="utf-8")
)
old_pyproject_toml = tomlkit.loads(
(tempdir / "pyproject.toml").read_text(encoding="utf-8")
)

old_pyproject_toml["tool"]["poetry"].pop("version") # type: ignore[attr-defined]
new_version = new_pyproject_toml["tool"]["poetry"].pop( # type: ignore[attr-defined] # type: ignore[attr-defined]
"version"
)

assert old_pyproject_toml == new_pyproject_toml
assert new_version == expected_new_version

# Compare __init__.py
new_init_py = (
(example_project / "src" / EXAMPLE_PROJECT_NAME / "__init__.py")
.read_text(encoding="utf-8")
.splitlines(keepends=True)
)
old_init_py = (
(tempdir / "src" / EXAMPLE_PROJECT_NAME / "__init__.py")
.read_text(encoding="utf-8")
.splitlines(keepends=True)
)

d = difflib.Differ()
diff = list(d.compare(old_init_py, new_init_py))
added = [line[2:] for line in diff if line.startswith("+ ")]
removed = [line[2:] for line in diff if line.startswith("- ")]

assert len(removed) == 1
assert re.match('__version__ = ".*"', removed[0])
assert added == [f'__version__ = "{expected_new_version}"\n']

0 comments on commit de6b9ad

Please sign in to comment.