Skip to content

feat(github-actions): add commit_sha as a GitHub Actions output value #1289

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
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions docs/configuration/automatic-releases/github-actions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand Down
3 changes: 3 additions & 0 deletions src/semantic_release/cli/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/semantic_release/cli/github_actions_output.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -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()])
Expand Down
35 changes: 27 additions & 8 deletions tests/e2e/cmd_version/test_version_github_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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"]
Expand All @@ -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."
Expand All @@ -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"]
21 changes: 20 additions & 1 deletion tests/unit/semantic_release/cli/test_github_actions_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
)
Expand All @@ -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()
Expand All @@ -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(
Expand All @@ -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)
Expand Down
Loading