From cb037aa69c0d59f30fc9e57d5941c2a01f2c9714 Mon Sep 17 00:00:00 2001 From: emil Date: Wed, 24 Jun 2026 19:02:02 +0000 Subject: [PATCH] DEVX-38: fix: clean dist/ before build and add workflow_dispatch to publish --- .gitea/workflows/publish.yml | 8 +++++++- src/devx/ci/publish.py | 7 +++++++ tests/unit/test_publish.py | 17 +++++++++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml index 6875bd7..b61c8cb 100644 --- a/.gitea/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -4,6 +4,12 @@ on: push: tags: - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to publish (e.g. v0.9.11)' + required: true + type: string jobs: publish: @@ -34,7 +40,7 @@ jobs: PYTHONPATH: src run: | export PATH="$HOME/.local/bin:$PATH" - python3 -m devx.ci.publish "${{ github.ref_name }}" "${{ github.repository }}" + python3 -m devx.ci.publish "${{ github.event.inputs.tag || github.ref_name }}" "${{ github.repository }}" - name: Notify on failure if: failure() env: diff --git a/src/devx/ci/publish.py b/src/devx/ci/publish.py index 748c9af..4fed5d5 100644 --- a/src/devx/ci/publish.py +++ b/src/devx/ci/publish.py @@ -23,6 +23,7 @@ import os import shutil import subprocess # nosec B404 import sys +from pathlib import Path import click from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType] @@ -60,6 +61,12 @@ def generate_release_notes(tag: str) -> str: def build_package() -> None: """Build the Python package using python -m build.""" + # Clean dist/ to avoid uploading stale packages from previous builds + # (Gitea PyPI returns 409 Conflict for already-published versions). + dist_dir = Path("dist") + if dist_dir.exists(): + shutil.rmtree(dist_dir) + result = subprocess.run( # nosec B603 [sys.executable, "-m", "build"], capture_output=True, diff --git a/tests/unit/test_publish.py b/tests/unit/test_publish.py index a0bcdef..416508f 100644 --- a/tests/unit/test_publish.py +++ b/tests/unit/test_publish.py @@ -1,5 +1,6 @@ """Unit tests for devx.ci.publish.""" +from pathlib import Path from unittest.mock import MagicMock, patch import click @@ -64,7 +65,8 @@ class TestGenerateReleaseNotes: class TestBuildPackage: @patch("devx.ci.publish.subprocess.run") - def test_success(self, mock_run: MagicMock) -> None: + @patch("devx.ci.publish.Path.exists", return_value=False) + def test_success(self, mock_exists: MagicMock, mock_run: MagicMock) -> None: mock_run.return_value = MagicMock(returncode=0, stderr="") build_package() args, _ = mock_run.call_args @@ -72,12 +74,23 @@ class TestBuildPackage: assert args[0][2] == "build" @patch("devx.ci.publish.subprocess.run") - def test_failure_raises(self, mock_run: MagicMock) -> None: + @patch("devx.ci.publish.Path.exists", return_value=False) + def test_failure_raises(self, mock_exists: MagicMock, mock_run: MagicMock) -> None: mock_run.return_value = MagicMock(returncode=1, stderr="build error") with pytest.raises(click.ClickException) as exc: build_package() assert "build" in str(exc.value) + @patch("devx.ci.publish.subprocess.run") + @patch("devx.ci.publish.shutil.rmtree") + @patch("devx.ci.publish.Path.exists", return_value=True) + def test_cleans_dist_before_build( + self, mock_exists: MagicMock, mock_rmtree: MagicMock, mock_run: MagicMock + ) -> None: + mock_run.return_value = MagicMock(returncode=0, stderr="") + build_package() + mock_rmtree.assert_called_once_with(Path("dist")) + class TestPublishToPypi: @patch("devx.ci.publish.subprocess.run")