Skip to content

Commit

Permalink
fix: enable --print-last-released* when in detached head or non-rel…
Browse files Browse the repository at this point in the history
…ease branch (python-semantic-release#926)

* test(version-cmd): add tests to print when detached or non-release branch

  ref: python-semantic-release#900

* fix(version-cmd): drop branch restriction for `--print-last-released*` opts

  Resolves: python-semantic-release#900
  • Loading branch information
codejedi365 authored Jul 6, 2024
1 parent bfda159 commit 782c0a6
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 22 deletions.
38 changes: 29 additions & 9 deletions semantic_release/cli/cli_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,17 @@ def __init__(
logger: logging.Logger,
global_opts: GlobalCommandLineOptions,
) -> None:
self._runtime_ctx: RuntimeContext | None = None
self.ctx = ctx
self.logger = logger
self.global_opts = global_opts
self._raw_config: RawConfig | None = None
self._runtime_ctx: RuntimeContext | None = None

@property
def raw_config(self) -> RawConfig:
if self._raw_config is None:
self._raw_config = self._init_raw_config()
return self._raw_config

@property
def runtime_ctx(self) -> RuntimeContext:
Expand All @@ -49,7 +56,7 @@ def runtime_ctx(self) -> RuntimeContext:
self._runtime_ctx = self._init_runtime_ctx()
return self._runtime_ctx

def _init_runtime_ctx(self) -> RuntimeContext:
def _init_raw_config(self) -> RawConfig:
config_path = Path(self.global_opts.config_file)
conf_file_exists = config_path.exists()
was_conf_file_user_provided = bool(
Expand All @@ -60,6 +67,7 @@ def _init_runtime_ctx(self) -> RuntimeContext:
)
)

# TODO: Evaluate Exeception catches
try:
if was_conf_file_user_provided and not conf_file_exists:
raise FileNotFoundError( # noqa: TRY301
Expand All @@ -74,24 +82,36 @@ def _init_runtime_ctx(self) -> RuntimeContext:
"configuration empty, falling back to default configuration"
)

raw_config = RawConfig.model_validate(config_obj)
return RawConfig.model_validate(config_obj)
except FileNotFoundError as exc:
click.echo(str(exc), err=True)
self.ctx.exit(2)
except (
ValidationError,
InvalidConfiguration,
InvalidGitRepositoryError,
) as exc:
click.echo(str(exc), err=True)
self.ctx.exit(1)

def _init_runtime_ctx(self) -> RuntimeContext:
# TODO: Evaluate Exception catches
try:
runtime = RuntimeContext.from_raw_config(
raw_config,
self.raw_config,
global_cli_options=self.global_opts,
)
except (DetachedHeadGitError, NotAReleaseBranch) as exc:
except NotAReleaseBranch as exc:
rprint(f"[bold {'red' if self.global_opts.strict else 'orange1'}]{exc!s}")
# If not strict, exit 0 so other processes can continue. For example, in
# multibranch CI it might be desirable to run a non-release branch's pipeline
# without specifying conditional execution of PSR based on branch name
self.ctx.exit(2 if self.global_opts.strict else 0)
except FileNotFoundError as exc:
click.echo(str(exc), err=True)
self.ctx.exit(2)
except (
ValidationError,
DetachedHeadGitError,
InvalidConfiguration,
InvalidGitRepositoryError,
ValidationError,
) as exc:
click.echo(str(exc), err=True)
self.ctx.exit(1)
Expand Down
27 changes: 18 additions & 9 deletions semantic_release/cli/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,26 +413,28 @@ def version( # noqa: C901
* Create a release (if supported) in the remote VCS for this tag
"""
ctx = click.get_current_context()
runtime = cli_ctx.runtime_ctx
translator = runtime.version_translator

# Enable any cli overrides of configuration before asking for the runtime context
config = cli_ctx.raw_config

# We can short circuit updating the release if we are only printing the last released version
if print_last_released or print_last_released_tag:
# TODO: get tag format a better way
if not (last_release := last_released(runtime.repo_dir, translator.tag_format)):
if not (
last_release := last_released(config.repo_dir, tag_format=config.tag_format)
):
log.warning("No release tags found.")
return

click.echo(last_release[0] if print_last_released_tag else last_release[1])
return

# TODO: figure out --print of next version with & without branch validation
# do you always need a prerelease token if its not --as-prerelease?
runtime = cli_ctx.runtime_ctx
translator = runtime.version_translator

parser = runtime.commit_parser
forced_level_bump = None if not force_level else LevelBump.from_string(force_level)
prerelease = is_forced_prerelease(
as_prerelease=as_prerelease,
forced_level_bump=forced_level_bump,
prerelease=runtime.prerelease,
)
hvcs_client = runtime.hvcs_client
assets = runtime.assets
commit_author = runtime.commit_author
Expand All @@ -442,6 +444,13 @@ def version( # noqa: C901
opts = runtime.global_cli_options
gha_output = VersionGitHubActionsOutput(released=False)

forced_level_bump = None if not force_level else LevelBump.from_string(force_level)
prerelease = is_forced_prerelease(
as_prerelease=as_prerelease,
forced_level_bump=forced_level_bump,
prerelease=runtime.prerelease,
)

if prerelease_token:
log.info("Forcing use of %s as the prerelease token", prerelease_token)
translator.prerelease_token = prerelease_token
Expand Down
4 changes: 2 additions & 2 deletions tests/command_line/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ def test_not_a_release_branch_detached_head_exit_code(
cli_cmd = [MAIN_PROG_NAME, VERSION_SUBCMD, "--no-commit"]
result = cli_runner.invoke(main, cli_cmd[1:])

# as non-strict, this will return success exit code
assert_successful_exit_code(result, cli_cmd)
# detached head states should throw an error as release branches cannot be determined
assert_exit_code(1, result, cli_cmd)
assert expected_err_msg in result.stderr


Expand Down
132 changes: 130 additions & 2 deletions tests/command_line/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from pytest_lazyfixture import lazy_fixture

from semantic_release.cli.commands.main import main
from semantic_release.hvcs.github import Github

from tests.const import (
EXAMPLE_PROJECT_NAME,
Expand Down Expand Up @@ -53,6 +54,7 @@
UpdatePyprojectTomlFn,
UseReleaseNotesTemplateFn,
)
from tests.fixtures.git_repo import SimulateChangeCommitsNReturnChangelogEntryFn


@pytest.mark.parametrize(
Expand Down Expand Up @@ -1001,6 +1003,33 @@ def test_version_exit_code_when_not_strict(
assert_successful_exit_code(result, cli_cmd)


@pytest.mark.parametrize(
"is_strict, exit_code", [(True, 2), (False, 0)], ids=["strict", "non-strict"]
)
def test_version_on_nonrelease_branch(
repo_with_single_branch_angular_commits: Repo,
cli_runner: CliRunner,
is_strict: bool,
exit_code: int,
):
branch = repo_with_single_branch_angular_commits.create_head("next")
branch.checkout()
expected_error_msg = (
f"branch '{branch.name}' isn't in any release groups; no release will be made\n"
)

# Act
cli_cmd = list(
filter(None, [MAIN_PROG_NAME, "--strict" if is_strict else "", VERSION_SUBCMD])
)
result = cli_runner.invoke(main, cli_cmd[1:])

# Evaluate (expected -> actual)
assert_exit_code(exit_code, result, cli_cmd)
assert not result.stdout
assert expected_error_msg == result.stderr


def test_custom_release_notes_template(
mocked_git_push: MagicMock,
repo_with_no_tags_angular_commits: Repo,
Expand All @@ -1017,7 +1046,7 @@ def test_custom_release_notes_template(
runtime_context_with_no_tags = retrieve_runtime_context(
repo_with_no_tags_angular_commits
)
cli_cmd = ["semantic-release", VERSION_SUBCMD, "--skip-build", "--vcs-release"]
cli_cmd = [MAIN_PROG_NAME, VERSION_SUBCMD, "--skip-build", "--vcs-release"]

# Act
result = cli_runner.invoke(main, cli_cmd[1:])
Expand Down Expand Up @@ -1056,7 +1085,7 @@ def test_version_tag_only_push(

# Act
cli_cmd = [
"semantic-release",
MAIN_PROG_NAME,
VERSION_SUBCMD,
"--tag",
"--no-commit",
Expand Down Expand Up @@ -1219,3 +1248,102 @@ def test_version_print_last_released_prints_nothing_if_no_tags(
assert_successful_exit_code(result, cli_cmd)
assert result.stdout == ""
assert "No release tags found." in caplog.text


def test_version_print_last_released_on_detached_head(
cli_runner: CliRunner,
repo_with_single_branch_tag_commits: Repo,
):
last_version = "0.1.1"
repo_with_single_branch_tag_commits.git.checkout("HEAD", detach=True)

cli_cmd = [MAIN_PROG_NAME, VERSION_SUBCMD, "--print-last-released"]

# Act
result = cli_runner.invoke(main, cli_cmd[1:])

# Evaluate (expected -> actual)
assert_successful_exit_code(result, cli_cmd)
assert not result.stderr
assert last_version == result.stdout.rstrip()


def test_version_print_last_released_on_nonrelease_branch(
cli_runner: CliRunner,
repo_with_single_branch_tag_commits: Repo,
):
last_version = "0.1.1"
repo_with_single_branch_tag_commits.create_head("next").checkout()

cli_cmd = [MAIN_PROG_NAME, VERSION_SUBCMD, "--print-last-released"]

# Act
result = cli_runner.invoke(main, cli_cmd[1:])

# Evaluate (expected -> actual)
assert_successful_exit_code(result, cli_cmd)
assert not result.stderr
assert last_version == result.stdout.rstrip()


def test_version_print_last_released_tag_on_detached_head(
cli_runner: CliRunner,
repo_with_single_branch_tag_commits: Repo,
):
last_version = "v0.1.1"
repo_with_single_branch_tag_commits.git.checkout("HEAD", detach=True)

cli_cmd = [MAIN_PROG_NAME, VERSION_SUBCMD, "--print-last-released-tag"]

# Act
result = cli_runner.invoke(main, cli_cmd[1:])

# Evaluate (expected -> actual)
assert_successful_exit_code(result, cli_cmd)
assert not result.stderr
assert last_version == result.stdout.rstrip()


def test_version_print_last_released_tag_on_nonrelease_branch(
cli_runner: CliRunner,
repo_with_single_branch_tag_commits: Repo,
):
last_version_tag = "v0.1.1"
repo_with_single_branch_tag_commits.create_head("next").checkout()

cli_cmd = [MAIN_PROG_NAME, VERSION_SUBCMD, "--print-last-released-tag"]

# Act
result = cli_runner.invoke(main, cli_cmd[1:])

# Evaluate (expected -> actual)
assert_successful_exit_code(result, cli_cmd)
assert not result.stderr
assert last_version_tag == result.stdout.rstrip()


def test_version_print_next_version_fails_on_detached_head(
cli_runner: CliRunner,
example_git_ssh_url: str,
repo_with_single_branch_tag_commits: Repo,
simulate_change_commits_n_rtn_changelog_entry: SimulateChangeCommitsNReturnChangelogEntryFn,
):
# Setup
repo_with_single_branch_tag_commits.git.checkout("HEAD", detach=True)
simulate_change_commits_n_rtn_changelog_entry(
repo_with_single_branch_tag_commits,
["fix: make a patch fix to codebase"],
Github(example_git_ssh_url),
)
expected_error_msg = (
"Detached HEAD state cannot match any release groups; no release will be made\n"
)

# Act
cli_cmd = [MAIN_PROG_NAME, VERSION_SUBCMD, "--print"]
result = cli_runner.invoke(main, cli_cmd[1:])

# Evaluate (expected -> actual)
assert_exit_code(1, result, cli_cmd)
assert not result.stdout
assert expected_error_msg == result.stderr

0 comments on commit 782c0a6

Please sign in to comment.