From 57734c9ef70a7965adffa840e35e8dc8b0271308 Mon Sep 17 00:00:00 2001 From: emil Date: Fri, 26 Jun 2026 01:30:22 +0200 Subject: [PATCH] fix: handle 'already a release' error idempotently in publish The tea CLI returns 'there is already a release for this tag' when a release already exists. The list_releases check can fail (e.g. tea JSON parsing issues), so create_release must also handle this case gracefully instead of raising an error. --- src/devx/ci/publish.py | 3 +++ tests/unit/test_publish.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/devx/ci/publish.py b/src/devx/ci/publish.py index f6b6981..38b51fa 100644 --- a/src/devx/ci/publish.py +++ b/src/devx/ci/publish.py @@ -298,6 +298,9 @@ def main( try: tea.create_release(repo, tag=tag, title=tag, body=release_body) except TeaCLIError as e: + if "already" in str(e).lower() and "release" in str(e).lower(): + click.echo(_("Gitea release {tag} already exists — skipping creation.", tag=tag)) + return raise click.ClickException(_("Release creation failed: {error}", error=str(e))) from None click.echo( diff --git a/tests/unit/test_publish.py b/tests/unit/test_publish.py index a86b3d9..755c515 100644 --- a/tests/unit/test_publish.py +++ b/tests/unit/test_publish.py @@ -388,6 +388,42 @@ class TestMain: result = runner.invoke(main, ["v1.0.0", "owner/repo"]) assert result.exit_code == 0 + @patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"}) + @patch("devx.ci.publish.generate_release_notes", return_value="Release notes") + @patch("devx.ci.publish.TeaCLI") + @patch("devx.ci.publish.publish_to_pypi") + @patch("devx.ci.publish.build_package") + def test_create_release_already_exists_is_idempotent( + self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock + ) -> None: + """If create_release fails with 'already exists', treat as success.""" + mock_tea = MagicMock() + mock_tea.list_releases.side_effect = TeaCLIError("api error") + mock_tea.create_release.side_effect = TeaCLIError("there is already a release for this tag") + mock_tea_cls.return_value = mock_tea + runner = CliRunner() + result = runner.invoke(main, ["v1.0.0", "owner/repo"]) + assert result.exit_code == 0 + assert "already exists" in result.output + + @patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"}) + @patch("devx.ci.publish.generate_release_notes", return_value="Release notes") + @patch("devx.ci.publish.TeaCLI") + @patch("devx.ci.publish.publish_to_pypi") + @patch("devx.ci.publish.build_package") + def test_create_release_other_error_raises( + self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock + ) -> None: + """If create_release fails with a non-'already exists' error, raise.""" + mock_tea = MagicMock() + mock_tea.list_releases.side_effect = TeaCLIError("api error") + mock_tea.create_release.side_effect = TeaCLIError("network error") + mock_tea_cls.return_value = mock_tea + runner = CliRunner() + result = runner.invoke(main, ["v1.0.0", "owner/repo"]) + assert result.exit_code != 0 + assert "Release creation failed" in result.output + class TestFromTag: def test_get_latest_tag_success(self) -> None: -- 2.54.0