diff --git a/pontos/github/actions/core.py b/pontos/github/actions/core.py index 78d17b98..091f9a1c 100644 --- a/pontos/github/actions/core.py +++ b/pontos/github/actions/core.py @@ -15,10 +15,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import contextlib import os +from contextlib import contextmanager +from io import TextIOWrapper from pathlib import Path -from typing import Optional +from typing import Generator, Optional from pontos.github.actions.errors import GitHubActionsError @@ -68,7 +69,7 @@ class Console: """ @classmethod - @contextlib.contextmanager + @contextmanager def group(cls, title: str): """ ContextManager to display a foldable group @@ -199,7 +200,49 @@ def debug(message: str): print(f"::debug::{message}") +class ActionOutput: + def __init__(self, file: TextIOWrapper) -> None: + self._file = file + + def write(self, name: str, value: str): + """ + Set action output + + An action output can be consumed by another job + + Args: + name: Name of the output variable + value: Value of the output variable + """ + self._file.write(f"{name}={value}\n") + + class ActionIO: + @staticmethod + def has_output() -> bool: + """ + Check if GITHUB_OUTPUT is set + """ + return "GITHUB_OUTPUT" in os.environ + + @staticmethod + @contextmanager + def out() -> Generator[ActionOutput, None, None]: + """ + Create action output + + An action output can be consumed by another job + """ + output_filename = os.environ.get("GITHUB_OUTPUT") + if not output_filename: + raise GitHubActionsError( + "GITHUB_OUTPUT environment variable not set. Can't write " + "action output." + ) + + with Path(output_filename).open("a", encoding="utf8") as f: + yield ActionOutput(f) + @staticmethod def output(name: str, value: str): """ diff --git a/pontos/release/release.py b/pontos/release/release.py index f3c806ac..9426d43d 100644 --- a/pontos/release/release.py +++ b/pontos/release/release.py @@ -18,6 +18,7 @@ import asyncio from argparse import Namespace +from dataclasses import dataclass from enum import IntEnum, auto from pathlib import Path from typing import Optional @@ -27,6 +28,7 @@ from pontos.changelog.conventional_commits import ChangelogBuilder from pontos.errors import PontosError from pontos.git import Git +from pontos.github.actions.core import ActionIO from pontos.github.api import GitHubAsyncRESTApi from pontos.terminal import Terminal from pontos.version import Version, VersionCalculator, VersionError @@ -37,6 +39,21 @@ from .helper import ReleaseType, find_signing_key, get_git_repository_name +@dataclass +class ReleaseInformation: + last_release_version: Version + release_version: Version + git_release_tag: str + next_version: Version + + def write_github_output(self): + with ActionIO.out() as output: + output.write("last-release-version", self.last_release_version) + output.write("release-version", self.release_version) + output.write("git-release-tag", self.git_release_tag) + output.write("next-version", self.next_version) + + class ReleaseReturnValue(IntEnum): """ Possible return values of ReleaseCommand @@ -345,6 +362,15 @@ async def run( self.terminal.info("Pushing changes") self.git.push(follow_tags=True, remote=git_remote_name) + self.release_information = ReleaseInformation( + last_release_version=last_release_version, + release_version=release_version, + git_release_tag=git_version, + next_version=next_version, + ) + if ActionIO.has_output(): + self.release_information.write_github_output() + return ReleaseReturnValue.SUCCESS diff --git a/tests/github/actions/test_core.py b/tests/github/actions/test_core.py index bfe7def9..c3e30009 100644 --- a/tests/github/actions/test_core.py +++ b/tests/github/actions/test_core.py @@ -119,14 +119,45 @@ def test_output(self): self.assertEqual(output, "foo=bar\nlorem=ipsum\n") + @patch.dict("os.environ", {}, clear=True) def test_output_no_env(self): - with patch.dict("os.environ", {}, clear=True), self.assertRaises( - GitHubActionsError - ): + with self.assertRaises(GitHubActionsError): ActionIO.output("foo", "bar") + @patch.dict("os.environ", {"GITHUB_OUTPUT": ""}, clear=True) def test_output_empty_env(self): - with patch.dict( - "os.environ", {"GITHUB_OUTPUT": ""}, clear=True - ), self.assertRaises(GitHubActionsError): + with self.assertRaises(GitHubActionsError): ActionIO.output("foo", "bar") + + @patch.dict("os.environ", {}, clear=True) + def test_no_github_output(self): + self.assertFalse(ActionIO.has_output()) + + @patch.dict( + "os.environ", {"GITHUB_OUTPUT": "/foo/github.output"}, clear=True + ) + def test_has_github_output(self): + self.assertTrue(ActionIO.has_output()) + + def test_out(self): + with temp_directory() as temp_dir: + outfile = temp_dir / "github.output" + with patch.dict( + "os.environ", + {"GITHUB_OUTPUT": str(outfile.absolute())}, + clear=True, + ): + with ActionIO.out() as output: + output.write("foo", "bar") + + self.assertEqual(outfile.read_text(encoding="utf8"), "foo=bar\n") + + @patch.dict("os.environ", {}, clear=True) + def test_out_failure(self): + with self.assertRaisesRegex( + GitHubActionsError, + "GITHUB_OUTPUT environment variable not set. Can't write " + "action output.", + ): + with ActionIO.out(): + pass diff --git a/tests/release/test_release.py b/tests/release/test_release.py index 43cef6c2..d1420645 100644 --- a/tests/release/test_release.py +++ b/tests/release/test_release.py @@ -29,10 +29,15 @@ from pontos.git.git import ConfigScope, Git from pontos.git.status import StatusEntry +from pontos.github.actions.errors import GitHubActionsError from pontos.release.main import parse_args -from pontos.release.release import ReleaseReturnValue, release +from pontos.release.release import ( + ReleaseInformation, + ReleaseReturnValue, + release, +) from pontos.terminal.terminal import Terminal -from pontos.testing import temp_git_repository +from pontos.testing import temp_directory, temp_git_repository from pontos.version import VersionError, VersionUpdate from pontos.version.commands import GoVersionCommand from pontos.version.schemes._pep440 import PEP440Version, PEP440VersioningScheme @@ -76,6 +81,68 @@ def setup_go_project( yield tmp_git +class ReleaseInformationTestCase(unittest.TestCase): + def test_release_info(self): + release_info = ReleaseInformation( + last_release_version=PEP440Version.from_string("1.2.3"), + release_version=PEP440Version.from_string("2.0.0"), + git_release_tag="v2.0.0", + next_version=PEP440Version.from_string("2.0.1.dev1"), + ) + + self.assertEqual( + release_info.last_release_version, + PEP440Version.from_string("1.2.3"), + ) + self.assertEqual( + release_info.release_version, PEP440Version.from_string("2.0.0") + ) + self.assertEqual(release_info.git_release_tag, "v2.0.0") + self.assertEqual( + release_info.next_version, PEP440Version.from_string("2.0.1.dev1") + ) + + @patch.dict("os.environ", {}, clear=True) + def test_no_github_output(self): + release_info = ReleaseInformation( + last_release_version=PEP440Version.from_string("1.2.3"), + release_version=PEP440Version.from_string("2.0.0"), + git_release_tag="v2.0.0", + next_version=PEP440Version.from_string("2.0.1.dev1"), + ) + + with self.assertRaisesRegex( + GitHubActionsError, + "GITHUB_OUTPUT environment variable not set. Can't write " + "action output.", + ): + release_info.write_github_output() + + def test_github_output(self): + expected = """last-release-version=1.2.3 +release-version=2.0.0 +git-release-tag=v2.0.0 +next-version=2.0.1.dev1 +""" + with temp_directory() as temp_dir: + out_file = temp_dir / "out.txt" + with patch.dict( + "os.environ", {"GITHUB_OUTPUT": str(out_file.absolute())} + ): + release_info = ReleaseInformation( + last_release_version=PEP440Version.from_string("1.2.3"), + release_version=PEP440Version.from_string("2.0.0"), + git_release_tag="v2.0.0", + next_version=PEP440Version.from_string("2.0.1.dev1"), + ) + + release_info.write_github_output() + + self.assertTrue(out_file.exists()) + actual = out_file.read_text(encoding="utf8") + self.assertEqual(actual, expected) + + @patch.dict( "os.environ", {