diff --git a/action.yml b/action.yml index 2cf7cf5ec..023aec9b7 100644 --- a/action.yml +++ b/action.yml @@ -130,6 +130,10 @@ outputs: description: | "true" if a release was made, "false" otherwise + commit_sha: + description: | + The commit SHA of the release if a release was made, otherwise an empty string + tag: description: | The Git tag corresponding to the version output diff --git a/docs/configuration/automatic-releases/github-actions.rst b/docs/configuration/automatic-releases/github-actions.rst index d9a00f1f5..07d29bce4 100644 --- a/docs/configuration/automatic-releases/github-actions.rst +++ b/docs/configuration/automatic-releases/github-actions.rst @@ -535,6 +535,20 @@ A boolean value indicating whether a release was made. ---- +.. _gh_actions-psr-outputs-commit_sha: + +``commit_sha`` +""""""""""""""""" + +**Type:** ``string`` + +The commit SHA of the release if a release was made, otherwise an empty string. + +Example upon release: ``d4c3b2a1e0f9c8b7a6e5d4c3b2a1e0f9c8b7a6e5`` +Example when no release was made: ``""`` + +---- + .. _gh_actions-psr-outputs-version: ``version`` diff --git a/src/semantic_release/cli/commands/version.py b/src/semantic_release/cli/commands/version.py index afa3f9b3d..45d9178b3 100644 --- a/src/semantic_release/cli/commands/version.py +++ b/src/semantic_release/cli/commands/version.py @@ -676,6 +676,9 @@ def version( # noqa: C901 noop=opts.noop, ) + with Repo(str(runtime.repo_dir)) as git_repo: + gha_output.commit_sha = git_repo.head.commit.hexsha + if push_changes: remote_url = runtime.hvcs_client.remote_url( use_token=not runtime.ignore_token_for_push diff --git a/src/semantic_release/cli/github_actions_output.py b/src/semantic_release/cli/github_actions_output.py index 7d7782922..9c4090d31 100644 --- a/src/semantic_release/cli/github_actions_output.py +++ b/src/semantic_release/cli/github_actions_output.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from re import compile as regexp from semantic_release.globals import logger from semantic_release.version.version import Version @@ -13,9 +14,11 @@ def __init__( self, released: bool | None = None, version: Version | None = None, + commit_sha: str | None = None, ) -> None: self._released = released self._version = version + self._commit_sha = commit_sha @property def released(self) -> bool | None: @@ -45,12 +48,30 @@ def tag(self) -> str | None: def is_prerelease(self) -> bool | None: return self.version.is_prerelease if self.version is not None else None + @property + def commit_sha(self) -> str | None: + return self._commit_sha if self._commit_sha else None + + @commit_sha.setter + def commit_sha(self, value: str) -> None: + if not isinstance(value, str): + raise TypeError("output 'commit_sha' should be a string") + + if not regexp(r"^[0-9a-f]{40}$").match(value): + raise ValueError( + "output 'commit_sha' should be a valid 40-hex-character SHA" + ) + + self._commit_sha = value + def to_output_text(self) -> str: missing = set() if self.version is None: missing.add("version") if self.released is None: missing.add("released") + if self.released and self.commit_sha is None: + missing.add("commit_sha") if missing: raise ValueError( @@ -62,6 +83,7 @@ def to_output_text(self) -> str: "version": str(self.version), "tag": self.tag, "is_prerelease": str(self.is_prerelease).lower(), + "commit_sha": self.commit_sha if self.commit_sha else "", } return str.join("", [f"{key}={value!s}\n" for key, value in outputs.items()]) diff --git a/tests/e2e/cmd_version/test_version_github_actions.py b/tests/e2e/cmd_version/test_version_github_actions.py index 53917e706..11f13b90f 100644 --- a/tests/e2e/cmd_version/test_version_github_actions.py +++ b/tests/e2e/cmd_version/test_version_github_actions.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING import pytest +from pytest_lazy_fixtures.lazy_fixture import lf as lazy_fixture from tests.const import MAIN_PROG_NAME, VERSION_SUBCMD from tests.fixtures.repos import ( @@ -13,16 +14,26 @@ if TYPE_CHECKING: from tests.conftest import RunCliFn from tests.fixtures.example_project import ExProjectDir + from tests.fixtures.git_repo import BuiltRepoResult -@pytest.mark.usefixtures( - repo_w_git_flow_w_alpha_prereleases_n_conventional_commits.__name__ +@pytest.mark.parametrize( + "repo_result", + [lazy_fixture(repo_w_git_flow_w_alpha_prereleases_n_conventional_commits.__name__)], ) def test_version_writes_github_actions_output( + repo_result: BuiltRepoResult, run_cli: RunCliFn, example_project_dir: ExProjectDir, ): mock_output_file = example_project_dir / "action.out" + expected_gha_output = { + "released": str(True).lower(), + "version": "1.2.1", + "tag": "v1.2.1", + "commit_sha": "0" * 40, + "is_prerelease": str(False).lower(), + } # Act cli_cmd = [MAIN_PROG_NAME, VERSION_SUBCMD, "--patch", "--no-push"] @@ -31,6 +42,9 @@ def test_version_writes_github_actions_output( ) assert_successful_exit_code(result, cli_cmd) + # Update the expected output with the commit SHA + expected_gha_output["commit_sha"] = repo_result["repo"].head.commit.hexsha + if not mock_output_file.exists(): pytest.fail( f"Expected output file {mock_output_file} to be created, but it does not exist." @@ -42,9 +56,14 @@ def test_version_writes_github_actions_output( ) # Evaluate - assert "released" in action_outputs - assert action_outputs["released"] == "true" - assert "version" in action_outputs - assert action_outputs["version"] == "1.2.1" - assert "tag" in action_outputs - assert action_outputs["tag"] == "v1.2.1" + expected_keys = set(expected_gha_output.keys()) + actual_keys = set(action_outputs.keys()) + key_difference = expected_keys.symmetric_difference(actual_keys) + + assert not key_difference, f"Unexpected keys found: {key_difference}" + + assert expected_gha_output["released"] == action_outputs["released"] + assert expected_gha_output["version"] == action_outputs["version"] + assert expected_gha_output["tag"] == action_outputs["tag"] + assert expected_gha_output["is_prerelease"] == action_outputs["is_prerelease"] + assert expected_gha_output["commit_sha"] == action_outputs["commit_sha"] diff --git a/tests/unit/semantic_release/cli/test_github_actions_output.py b/tests/unit/semantic_release/cli/test_github_actions_output.py index 7d46f18ef..7b5cc2861 100644 --- a/tests/unit/semantic_release/cli/test_github_actions_output.py +++ b/tests/unit/semantic_release/cli/test_github_actions_output.py @@ -25,24 +25,27 @@ def test_version_github_actions_output_format( released: bool, version: str, is_prerelease: bool ): + commit_sha = "0" * 40 # 40 zeroes to simulate a SHA-1 hash expected_output = dedent( f"""\ released={'true' if released else 'false'} version={version} tag=v{version} is_prerelease={'true' if is_prerelease else 'false'} + commit_sha={commit_sha} """ ) output = VersionGitHubActionsOutput( released=released, version=Version.parse(version), + commit_sha=commit_sha, ) # Evaluate (expected -> actual) assert expected_output == output.to_output_text() -def test_version_github_actions_output_fails_if_missing_output(): +def test_version_github_actions_output_fails_if_missing_released_param(): output = VersionGitHubActionsOutput( version=Version.parse("1.2.3"), ) @@ -52,15 +55,28 @@ def test_version_github_actions_output_fails_if_missing_output(): output.to_output_text() +def test_version_github_actions_output_fails_if_missing_commit_sha_param(): + output = VersionGitHubActionsOutput( + released=True, + version=Version.parse("1.2.3"), + ) + + # Execute with expected failure + with pytest.raises(ValueError, match="required outputs were not set"): + output.to_output_text() + + def test_version_github_actions_output_writes_to_github_output_if_available( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ): mock_output_file = tmp_path / "action.out" version_str = "1.2.3" + commit_sha = "0" * 40 # 40 zeroes to simulate a SHA-1 hash monkeypatch.setenv("GITHUB_OUTPUT", str(mock_output_file.resolve())) output = VersionGitHubActionsOutput( version=Version.parse(version_str), released=True, + commit_sha=commit_sha, ) output.write_if_possible() @@ -73,6 +89,8 @@ def test_version_github_actions_output_writes_to_github_output_if_available( assert version_str == action_outputs["version"] assert str(True).lower() == action_outputs["released"] assert str(False).lower() == action_outputs["is_prerelease"] + assert f"v{version_str}" == action_outputs["tag"] + assert commit_sha == action_outputs["commit_sha"] def test_version_github_actions_output_no_error_if_not_in_gha( @@ -81,6 +99,7 @@ def test_version_github_actions_output_no_error_if_not_in_gha( output = VersionGitHubActionsOutput( version=Version.parse("1.2.3"), released=True, + commit_sha="0" * 40, # 40 zeroes to simulate a SHA-1 hash ) monkeypatch.delenv("GITHUB_OUTPUT", raising=False)
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: