Skip to content
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
1 change: 1 addition & 0 deletions src/semantic_release/cli/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ def version( # noqa: C901
)
except GitCommitEmptyIndexError:
logger.info("No local changes to add to any commit, skipping")
commit_changes = False

# Tag the version after potentially creating a new HEAD commit.
# This way if no source code is modified, i.e. all metadata updates
Expand Down
27 changes: 2 additions & 25 deletions tests/e2e/cmd_version/test_version_print.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ def test_version_print_next_version(
next_release_version: str,
file_in_repo: str,
run_cli: RunCliFn,
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
):
Expand Down Expand Up @@ -270,7 +269,6 @@ def test_version_print_tag_prints_next_tag(
get_cfg_value_from_def: GetCfgValueFromDefFn,
file_in_repo: str,
run_cli: RunCliFn,
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
):
Expand Down Expand Up @@ -386,7 +384,6 @@ def test_version_print_tag_prints_next_tag_no_zero_versions(
get_cfg_value_from_def: GetCfgValueFromDefFn,
file_in_repo: str,
run_cli: RunCliFn,
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
):
Expand Down Expand Up @@ -450,7 +447,6 @@ def test_version_print_last_released_prints_version(
repo_result: BuiltRepoResult,
get_versions_from_repo_build_def: GetVersionsFromRepoBuildDefFn,
run_cli: RunCliFn,
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
strip_logging_messages: StripLoggingMessagesFn,
Expand Down Expand Up @@ -502,7 +498,6 @@ def test_version_print_last_released_prints_released_if_commits(
get_versions_from_repo_build_def: GetVersionsFromRepoBuildDefFn,
commits: list[str],
run_cli: RunCliFn,
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
file_in_repo: str,
Expand Down Expand Up @@ -552,10 +547,8 @@ def test_version_print_last_released_prints_released_if_commits(
def test_version_print_last_released_prints_nothing_if_no_tags(
repo_result: BuiltRepoResult,
run_cli: RunCliFn,
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
caplog: pytest.LogCaptureFixture,
):
repo = repo_result["repo"]

Expand All @@ -577,10 +570,7 @@ def test_version_print_last_released_prints_nothing_if_no_tags(
# Evaluate (no release actions should have occurred on print)
assert_successful_exit_code(result, cli_cmd)
assert result.stdout == ""

# must use capture log to see this, because we use the logger to print this message
# not click's output
assert "No release tags found." in caplog.text
assert "No release tags found." in result.stderr

# assert nothing else happened (no code changes, no commit, no tag, no push, no vcs release)
assert repo_status_before == repo_status_after
Expand All @@ -598,7 +588,6 @@ def test_version_print_last_released_on_detached_head(
repo_result: BuiltRepoResult,
get_versions_from_repo_build_def: GetVersionsFromRepoBuildDefFn,
run_cli: RunCliFn,
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
strip_logging_messages: StripLoggingMessagesFn,
Expand Down Expand Up @@ -647,7 +636,6 @@ def test_version_print_last_released_on_nonrelease_branch(
repo_result: BuiltRepoResult,
get_versions_from_repo_build_def: GetVersionsFromRepoBuildDefFn,
run_cli: RunCliFn,
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
strip_logging_messages: StripLoggingMessagesFn,
Expand Down Expand Up @@ -705,7 +693,6 @@ def test_version_print_last_released_tag_prints_correct_tag(
get_cfg_value_from_def: GetCfgValueFromDefFn,
get_versions_from_repo_build_def: GetVersionsFromRepoBuildDefFn,
run_cli: RunCliFn,
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
strip_logging_messages: StripLoggingMessagesFn,
Expand Down Expand Up @@ -766,7 +753,6 @@ def test_version_print_last_released_tag_prints_released_if_commits(
get_versions_from_repo_build_def: GetVersionsFromRepoBuildDefFn,
commits: list[str],
run_cli: RunCliFn,
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
file_in_repo: str,
Expand Down Expand Up @@ -817,10 +803,8 @@ def test_version_print_last_released_tag_prints_released_if_commits(
def test_version_print_last_released_tag_prints_nothing_if_no_tags(
repo_result: BuiltRepoResult,
run_cli: RunCliFn,
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
caplog: pytest.LogCaptureFixture,
):
repo = repo_result["repo"]

Expand All @@ -842,10 +826,7 @@ def test_version_print_last_released_tag_prints_nothing_if_no_tags(
# Evaluate (no release actions should have occurred on print)
assert_successful_exit_code(result, cli_cmd)
assert result.stdout == ""

# must use capture log to see this, because we use the logger to print this message
# not click's output
assert "No release tags found." in caplog.text
assert "No release tags found." in result.stderr

# assert nothing else happened (no code changes, no commit, no tag, no push, no vcs release)
assert repo_status_before == repo_status_after
Expand All @@ -872,7 +853,6 @@ def test_version_print_last_released_tag_on_detached_head(
get_cfg_value_from_def: GetCfgValueFromDefFn,
get_versions_from_repo_build_def: GetVersionsFromRepoBuildDefFn,
run_cli: RunCliFn,
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
strip_logging_messages: StripLoggingMessagesFn,
Expand Down Expand Up @@ -931,7 +911,6 @@ def test_version_print_last_released_tag_on_nonrelease_branch(
get_cfg_value_from_def: GetCfgValueFromDefFn,
get_versions_from_repo_build_def: GetVersionsFromRepoBuildDefFn,
run_cli: RunCliFn,
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
strip_logging_messages: StripLoggingMessagesFn,
Expand Down Expand Up @@ -989,7 +968,6 @@ def test_version_print_next_version_fails_on_detached_head(
simulate_change_commits_n_rtn_changelog_entry: SimulateChangeCommitsNReturnChangelogEntryFn,
get_commit_def_fn: GetCommitDefFn[CommitParser[ParseResult, ParserOptions]],
default_parser: CommitParser[ParseResult, ParserOptions],
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
strip_logging_messages: StripLoggingMessagesFn,
Expand Down Expand Up @@ -1052,7 +1030,6 @@ def test_version_print_next_tag_fails_on_detached_head(
simulate_change_commits_n_rtn_changelog_entry: SimulateChangeCommitsNReturnChangelogEntryFn,
get_commit_def_fn: GetCommitDefFn[CommitParser[ParseResult, ParserOptions]],
default_parser: CommitParser[ParseResult, ParserOptions],
mocked_git_fetch: MagicMock,
mocked_git_push: MagicMock,
post_mocker: Mocker,
strip_logging_messages: StripLoggingMessagesFn,
Expand Down
159 changes: 159 additions & 0 deletions tests/e2e/cmd_version/test_version_upstream_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,165 @@ def test_version_upstream_check_success_no_changes_untracked_branch(
assert expected_vcs_url_post == post_mocker.call_count # one vcs release created


@pytest.mark.parametrize(
"repo_fixture_name, build_repo_fn",
[
(
repo_fixture_name,
lazy_fixture(build_repo_fn_name),
)
for repo_fixture_name, build_repo_fn_name in [
(
repo_w_trunk_only_conventional_commits.__name__,
build_trunk_only_repo_w_tags.__name__,
),
]
],
)
@pytest.mark.usefixtures(change_to_ex_proj_dir.__name__)
def test_version_no_upstream_check_on_no_version_commit(
repo_fixture_name: str,
run_cli: RunCliFn,
build_repo_fn: BuildSpecificRepoFn,
example_project_dir: ExProjectDir,
git_repo_for_directory: GetGitRepo4DirFn,
post_mocker: Mocker,
get_cfg_value_from_def: GetCfgValueFromDefFn,
get_versions_from_repo_build_def: GetVersionsFromRepoBuildDefFn,
pyproject_toml_file: Path,
update_pyproject_toml: UpdatePyprojectTomlFn,
):
"""
Test that PSR succeeds when no version commit is needed, so the upstream check is skipped.

This replicates the scenario that occurred on python-semantic-release/publish-action@v10.5.1
where the version command was run and no version commit was needed, but it failed because
it attempted to check the upstream branch anyway and we hard coded HEAD~1 because it expects
a version commit to be created. This is the only reason why you would check the upstream branch
because pushing a tag to the remote can happen even if the upstream branch has changed.
"""
remote_name = "origin"
# Create a bare remote (simulating origin)
local_origin = Repo.init(str(example_project_dir / "local_origin"), bare=True)

# build target repo into a temporary directory
target_repo_dir = example_project_dir / repo_fixture_name
commit_type: CommitConvention = (
repo_fixture_name.split("commits", 1)[0].split("_")[-2] # type: ignore[assignment]
)
target_repo_definition = build_repo_fn(
repo_name=repo_fixture_name,
commit_type=commit_type,
dest_dir=target_repo_dir,
)
target_git_repo = git_repo_for_directory(target_repo_dir)

# Configure the source repo to use the bare remote (removing any existing 'origin')
with contextlib.suppress(AttributeError):
target_git_repo.delete_remote(target_git_repo.remotes[remote_name])

target_git_repo.create_remote(remote_name, str(local_origin.working_dir))

# Remove last release before pushing to upstream
tag_format_str = cast(
"str", get_cfg_value_from_def(target_repo_definition, "tag_format_str")
)
latest_tag = tag_format_str.format(
version=get_versions_from_repo_build_def(target_repo_definition)[-1]
)
target_git_repo.git.tag("-d", latest_tag)
target_git_repo.git.reset("--hard", "HEAD~1")

# Remove any version variables to ensure no version commit is needed
update_pyproject_toml(
"tool.semantic_release.version_variables",
None,
target_repo_dir / pyproject_toml_file,
)
update_pyproject_toml(
"tool.semantic_release.version_toml",
None,
target_repo_dir / pyproject_toml_file,
)
# TODO: when available, switch this to use hvcs=none or similar config to avoid token use for push
update_pyproject_toml(
"tool.semantic_release.remote.ignore_token_for_push",
True,
target_repo_dir / pyproject_toml_file,
)
target_git_repo.git.commit(amend=True, no_edit=True, all=True)

# push the current state to establish the remote (cannot push tags and branches at the same time)
target_git_repo.git.push(remote_name, all=True) # all branches
target_git_repo.git.push(remote_name, tags=True) # all tags

# ensure bare remote HEAD points to the active branch so clones can checkout
local_origin.git.symbolic_ref(
"HEAD", f"refs/heads/{target_git_repo.active_branch.name}"
)

# Simulate CI environment after someone pushes to the repo
ci_commit_sha = target_git_repo.head.commit.hexsha
ci_branch = target_git_repo.active_branch.name

# current remote tags
remote_origin_tags_before = {tag.name for tag in local_origin.tags}

# Simulate a CI environment by fetching the repo to a new location
test_repo = Repo.init(str(example_project_dir / "ci_repo"))
with test_repo.config_writer("repository") as config:
config.set_value("core", "hookspath", "")
config.set_value("commit", "gpgsign", False)
config.set_value("tag", "gpgsign", False)

# Configure and retrieve the repository (see GitHub actions/checkout@v5)
test_repo.git.remote(
"add",
remote_name,
f"file:///{PureWindowsPath(local_origin.working_dir).as_posix()}",
)
test_repo.git.fetch("--depth=1", remote_name, ci_commit_sha)

# Simulate CI environment and recommended workflow (in docs)
# NOTE: this could be done in 1 step, but most CI pipelines are doing it in 2 steps
# 1. Checkout the commit sha (detached head)
test_repo.git.checkout(ci_commit_sha, force=True)
# 2. Forcefully set the branch to the current detached head
test_repo.git.checkout("-B", ci_branch)

# Act: run PSR on the cloned repo - it should verify upstream and succeed
with temporary_working_directory(str(test_repo.working_dir)):
# We don't use `--no-commit` here because we want to test that the upstream check is skipped
# when PSR determines that no version commit is needed. If we used `--no-commit`, it would skip the
# upstream check because it would think that a version commit was not needed.
cli_cmd = [
MAIN_PROG_NAME,
"--strict",
VERSION_SUBCMD,
"--no-changelog",
"--skip-build",
]
result = run_cli(cli_cmd[1:], env={Github.DEFAULT_ENV_TOKEN_NAME: "1234"})

remote_origin_tags_after = {tag.name for tag in local_origin.tags}

# Evaluate
assert_successful_exit_code(result, cli_cmd)

# Verify release occurred as expected
with test_repo:
assert latest_tag in test_repo.tags, "Expected release tag to be created"
assert (
ci_commit_sha == test_repo.head.commit.hexsha
), "Expected no new commit to be created on HEAD"
different_tags = remote_origin_tags_after.difference(remote_origin_tags_before)
assert latest_tag in different_tags, "Expected new tag to be pushed to remote"

# Verify VCS release was created
expected_vcs_url_post = 1
assert expected_vcs_url_post == post_mocker.call_count # one vcs release created


@pytest.mark.parametrize(
"repo_fixture_name, build_repo_fn",
[
Expand Down
Loading