Skip to content

fix: enable --print-last-released* when in detached head or non-release branch #926

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
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
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
Loading