Repository: relekang/python-semantic-release
Pull request #308 (2021-01-06 07:40:06)

feat(build): allow falsy values for build_command to disable build step
It may sounds strange to have no `build_command` at all, but my build is performed for 3 distincts OS (linux, macos, windows) with pyinstaller inside a first github action job, and then another job grabs generated artifacts to publish using semver. There's no build command inside PSR to perform because artifacts to publish are already produced by github action.


Patch:
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 4a51ebe..ccda4a1 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -377,7 +377,9 @@ Default: `true`
 -----------------
 Command to build dists. Build output should be stored in the directory configured in
 ``dist_path``.  If necessary, multiple commands can be specified using ``&&``, e.g.
-``pip install -m flit && flit build``.
+``pip install -m flit && flit build``. If set to false, build command is disabled and
+files should be placed manually in the directory configured in
+``dist_path``.
 
 Default: ``python setup.py sdist bdist_wheel``
 
diff --git a/semantic_release/cli.py b/semantic_release/cli.py
index ff25d3b..a934f2f 100644
--- a/semantic_release/cli.py
+++ b/semantic_release/cli.py
@@ -11,7 +11,7 @@ from semantic_release import ci_checks
 from semantic_release.errors import GitError, ImproperConfigurationError
 
 from .changelog import markdown_changelog
-from .dist import build_dists, remove_dists
+from .dist import build_dists, remove_dists, should_build, should_remove_dist
 from .history import (
     evaluate_version_bump,
     get_current_version,
@@ -258,7 +258,6 @@ def publish(**kwargs):
 
         # Get config options for uploads
         dist_path = config.get("dist_path")
-        remove_dist = config.get("remove_dist")
         upload_pypi = config.get("upload_to_pypi")
         upload_to_pypi_glob_patterns = config.get("upload_to_pypi_glob_patterns")
         upload_release = config.get("upload_to_release")
@@ -266,10 +265,10 @@ def publish(**kwargs):
         if upload_to_pypi_glob_patterns:
             upload_to_pypi_glob_patterns = upload_to_pypi_glob_patterns.split(",")
 
-        if upload_pypi or upload_release:
+        if should_build():
             # We need to run the command to build wheels for releasing
             logger.info("Building distributions")
-            if remove_dist:
+            if should_remove_dist():
                 # Remove old distributions before building
                 remove_dists(dist_path)
             build_dists()
@@ -298,7 +297,7 @@ def publish(**kwargs):
             logger.info("Uploading to HVCS release")
             upload_to_release(owner, name, new_version, dist_path)
         # Remove distribution files as they are no longer needed
-        if (upload_pypi or upload_release) and remove_dist:
+        if should_remove_dist():
             remove_dists(dist_path)
 
         logger.info("New release published")
diff --git a/semantic_release/dist.py b/semantic_release/dist.py
index bc5f597..ef38703 100644
--- a/semantic_release/dist.py
+++ b/semantic_release/dist.py
@@ -9,6 +9,18 @@ from .settings import config
 logger = logging.getLogger(__name__)
 
 
+def should_build():
+    upload_pypi = config.get("upload_to_pypi")
+    upload_release = config.get("upload_to_release")
+    build_command = config.get("build_command")
+    return bool(build_command and (upload_pypi or upload_release))
+
+
+def should_remove_dist():
+    remove_dist = config.get("remove_dist")
+    return bool(remove_dist and should_build())
+
+
 def build_dists():
     command = config.get("build_command")
     logger.info(f"Running {command}")


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index ed97e9c..384c114 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -408,6 +408,52 @@ def test_publish_should_call_functions(mocker):
     mock_checkout.assert_called_once_with("master")
 
 
+def test_publish_should_skip_build_when_command_is_empty(mocker):
+    mock_push = mocker.patch("semantic_release.cli.push_new_version")
+    mock_checkout = mocker.patch("semantic_release.cli.checkout")
+    mock_should_bump_version = mocker.patch(
+        "semantic_release.cli.should_bump_version", return_value=True
+    )
+    mock_log = mocker.patch("semantic_release.cli.post_changelog")
+    mock_ci_check = mocker.patch("semantic_release.ci_checks.check")
+    mock_pypi = mocker.patch("semantic_release.cli.upload_to_pypi")
+    mock_release = mocker.patch("semantic_release.cli.upload_to_release")
+    mock_build_dists = mocker.patch("semantic_release.cli.build_dists")
+    mock_remove_dists = mocker.patch("semantic_release.cli.remove_dists")
+    mocker.patch(
+        "semantic_release.cli.get_repository_owner_and_name",
+        return_value=("relekang", "python-semantic-release"),
+    )
+    mocker.patch("semantic_release.cli.evaluate_version_bump", lambda *x: "feature")
+    mocker.patch("semantic_release.cli.generate_changelog")
+    mocker.patch("semantic_release.cli.markdown_changelog", lambda *x, **y: "CHANGES")
+    mocker.patch("semantic_release.cli.update_changelog_file")
+    mocker.patch("semantic_release.cli.bump_version")
+    mocker.patch("semantic_release.cli.get_new_version", lambda *x: "2.0.0")
+    mocker.patch("semantic_release.cli.check_token", lambda: True)
+
+    mocker.patch(
+        "semantic_release.cli.config.get",
+        wrapped_config_get(
+            build_command="",
+        ),
+    )
+
+    publish()
+
+    assert mock_ci_check.called
+    assert mock_push.called
+    assert not mock_remove_dists.called
+    assert not mock_build_dists.called
+    assert mock_pypi.called
+    assert mock_release.called
+    assert mock_should_bump_version.called
+    mock_log.assert_called_once_with(
+        u"relekang", "python-semantic-release", "2.0.0", "CHANGES"
+    )
+    mock_checkout.assert_called_once_with("master")
+
+
 def test_publish_should_call_functions_with_custom_pypi_glob_patterns(mocker):
     mock_push = mocker.patch("semantic_release.cli.push_new_version")
     mock_checkout = mocker.patch("semantic_release.cli.checkout")
diff --git a/tests/test_dist.py b/tests/test_dist.py
index a9e562f..debf3d0 100644
--- a/tests/test_dist.py
+++ b/tests/test_dist.py
@@ -1,5 +1,4 @@
-from semantic_release.dist import build_dists
-
+from semantic_release.dist import build_dists, should_build, should_remove_dist
 from . import pytest
 
 
@@ -12,3 +11,66 @@ def test_build_command(mocker, commands):
     mock_run = mocker.patch("semantic_release.dist.run")
     build_dists()
     mock_run.assert_called_once_with(commands)
+
+
+@pytest.mark.parametrize(
+    "config,expected",
+    [
+        ({"upload_to_pypi": True,
+          "upload_to_release": True,
+          "build_command": "python setup.py build"},
+         True),
+        ({"upload_to_pypi": True,
+          "upload_to_release": True,
+          "build_command": False},
+         False),
+        ({"upload_to_pypi": True,
+          "upload_to_release": True,
+          "build_command": None},
+         False),
+        ({"upload_to_pypi": True,
+          "upload_to_release": True,
+          "build_command": ""},
+         False),
+        ({"upload_to_pypi": False,
+          "upload_to_release": True,
+          "build_command": "python setup.py build"},
+         True),
+        ({"upload_to_pypi": True,
+          "upload_to_release": False,
+          "build_command": "python setup.py build"},
+         True),
+        ({"upload_to_pypi": False,
+          "upload_to_release": False,
+          "build_command": "python setup.py build"},
+         False)
+    ]
+)
+def test_should_build(config, expected, mocker):
+    mocker.patch("semantic_release.cli.config.get", lambda key: config.get(key))
+    assert should_build() == expected
+
+
+@pytest.mark.parametrize(
+    "config,expected",
+    [
+        ({"upload_to_pypi": True,
+          "upload_to_release": True,
+          "build_command": "python setup.py build",
+          "remove_dist": True},
+         True),
+        ({"upload_to_pypi": True,
+          "upload_to_release": True,
+          "build_command": "python setup.py build",
+          "remove_dist": False},
+         False),
+        ({"upload_to_pypi": False,
+          "upload_to_release": False,
+          "build_command": False,
+          "remove_dist": True},
+         False),
+    ]
+)
+def test_should_remove_dist(config, expected, mocker):
+    mocker.patch("semantic_release.cli.config.get", lambda key: config.get(key))
+    assert should_remove_dist() == expected