Repository: SFDO-Tooling/CumulusCI
Pull request #3651 (2023-09-13 10:53:10)

Deploy Major and Minor Version option in upload_production task


Patch:
diff --git a/cumulusci/tasks/salesforce/package_upload.py b/cumulusci/tasks/salesforce/package_upload.py
index 4c3864611..f58029ee6 100644
--- a/cumulusci/tasks/salesforce/package_upload.py
+++ b/cumulusci/tasks/salesforce/package_upload.py
@@ -2,7 +2,11 @@ from datetime import datetime
 
 from cumulusci.cli.ui import CliTable
 from cumulusci.core.dependencies.resolvers import get_static_dependencies
-from cumulusci.core.exceptions import ApexTestException, SalesforceException
+from cumulusci.core.exceptions import (
+    ApexTestException,
+    SalesforceException,
+    TaskOptionsError,
+)
 from cumulusci.tasks.salesforce import BaseSalesforceApiTask
 
 
@@ -32,6 +36,14 @@ class PackageUpload(BaseSalesforceApiTask):
         "resolution_strategy": {
             "description": "The name of a sequence of resolution_strategy (from project__dependency_resolutions) to apply to dynamic dependencies. Defaults to 'production'."
         },
+        "major_version": {
+            "description": "The desired major version number for the uploaded package. Defaults to latest major version.",
+            "required": False,
+        },
+        "minor_version": {
+            "description": "The desired minor version number for the uploaded package. Defaults to next available minor version for the current major version.",
+            "required": False,
+        },
     }
 
     def _init_options(self, kwargs):
@@ -46,7 +58,10 @@ class PackageUpload(BaseSalesforceApiTask):
             self.options["namespace"] = self.project_config.project__package__namespace
 
     def _run_task(self):
+
+        self._validate_versions()
         self._set_package_id()
+
         self._set_package_info()
 
         self._make_package_upload_request()
@@ -59,6 +74,54 @@ class PackageUpload(BaseSalesforceApiTask):
             self._set_dependencies()
             self._log_package_upload_success()
 
+    def _validate_versions(self):
+        """This functions validates the major version and minor version if passed and sets them in the options dictionary if not passed."""
+
+        # Fetching the latest major,minor and patch version from Database
+        version = self._get_one_record(
+            (
+                """
+                SELECT MajorVersion,
+                MinorVersion,
+                PatchVersion,
+                ReleaseState
+                FROM MetadataPackageVersion
+                ORDER BY
+                MajorVersion DESC,
+                MinorVersion DESC,
+                PatchVersion DESC
+                LIMIT 1
+                """
+            ),
+            "Version not found",
+        )
+
+        # This if-else condition updates the major version to latest major version in options if not passed via command line and validates it if passed.
+        if "major_version" in self.options:
+            try:
+                if int(self.options["major_version"]) < version["MajorVersion"]:
+                    raise TaskOptionsError("Major Version not valid.")
+            except ValueError:
+                raise TaskOptionsError("Major Version not valid.")
+
+        else:
+            self.options["major_version"] = str(version["MajorVersion"])
+
+        # This if is executed only when major version is equal to latest major version. Updates the minor version in options if not passed and validates if passed
+        # Updates minor version when not passed in remaining cases.
+        if self.options["major_version"] == str(version["MajorVersion"]):
+            if "minor_version" in self.options:
+                if int(self.options["minor_version"]) <= version["MinorVersion"]:
+                    raise TaskOptionsError("Minor Version not valid.")
+            else:
+                if version["ReleaseState"] == "Beta":
+                    self.options["minor_version"] = str(version["MinorVersion"])
+                else:
+                    self.options["minor_version"] = str(version["MinorVersion"] + 1)
+        else:
+            if "minor_version" not in self.options:
+                self.options["minor_version"] = "0"
+
     def _set_package_info(self):
         if not self.package_id:
             self._set_package_id()
@@ -69,6 +132,8 @@ class PackageUpload(BaseSalesforceApiTask):
             "VersionName": self.options["name"],
             "IsReleaseVersion": production,
             "MetadataPackageId": self.package_id,
+            "MajorVersion": self.options["major_version"],
+            "MinorVersion": self.options["minor_version"],
         }
 
         if "description" in self.options:
@@ -208,6 +273,7 @@ class PackageUpload(BaseSalesforceApiTask):
             f"Version {self.version_id} not found",
         )
         version_parts = [str(version["MajorVersion"]), str(version["MinorVersion"])]
+
         if version["PatchVersion"]:
             version_parts.append(str(version["PatchVersion"]))
 


Test patch:
diff --git a/cumulusci/tasks/salesforce/tests/test_PackageUpload.py b/cumulusci/tasks/salesforce/tests/test_PackageUpload.py
index f33be0c16..c98a2e797 100644
--- a/cumulusci/tasks/salesforce/tests/test_PackageUpload.py
+++ b/cumulusci/tasks/salesforce/tests/test_PackageUpload.py
@@ -6,7 +6,11 @@ import pytest
 from cumulusci.core.config.org_config import OrgConfig, VersionInfo
 from cumulusci.core.config.project_config import BaseProjectConfig
 from cumulusci.core.config.universal_config import UniversalConfig
-from cumulusci.core.exceptions import ApexTestException, SalesforceException
+from cumulusci.core.exceptions import (
+    ApexTestException,
+    SalesforceException,
+    TaskOptionsError,
+)
 from cumulusci.tasks.salesforce import PackageUpload
 
 from .util import create_task
@@ -22,6 +26,8 @@ class TestPackageUpload:
                 "password": "pw",
                 "post_install_url": "http://www.salesforce.org",
                 "release_notes_url": "https://github.com",
+                "major_version": "2",
+                "minor_version": "11",
             },
         )
 
@@ -29,6 +35,18 @@ class TestPackageUpload:
             task.tooling = mock.Mock(
                 query=mock.Mock(
                     side_effect=[
+                        # Query for latest major and minor version used via validate version function
+                        {
+                            "totalSize": 1,
+                            "records": [
+                                {
+                                    "MajorVersion": 1,
+                                    "MinorVersion": 0,
+                                    "PatchVersion": 1,
+                                    "ReleaseState": "Released",
+                                }
+                            ],
+                        },
                         # Query for package by namespace
                         {"totalSize": 1, "records": [{"Id": "PKG_ID"}]},
                         # Query for upload status
@@ -75,6 +93,155 @@ class TestPackageUpload:
         task._set_package_id()
         assert expected_package_id == task.package_id
 
+    test_validate_version_base_options = {
+        "name": "Test Release",
+        "production": False,
+        "description": "Test Description",
+        "password": "secret",
+        "post_install_url": "post.install.url",
+        "release_notes_url": "release.notes.url",
+    }
+
+    def generate_valid_version_options(
+        major_version: str,
+        minor_version: str,
+        asserted_major_version: str,
+        asserted_minor_version: str,
+        is_negative: bool = False,
+    ):
+
+        """This is function is used to generate test cases for test_validate_versions(positive as well as negative)
+        Arguments:
+            major_version:acutal major version which is to be passed none if not passed.
+            minor_version:acutal minor version which is to be passed none if not passed
+            asserted_major_version:major version which is expected in options dict after running of validate version function.
+            asserted_minor_version:minor version which is expected in options dict after running of validate version function.
+            is_negative:This boolean is used to decide whether the function returns a postive or negative test case. defaults to true is not passed.
+        """
+        test_validate_version_base_options = {
+            "name": "Test Release",
+            "production": False,
+            "description": "Test Description",
+            "password": "secret",
+            "post_install_url": "post.install.url",
+            "release_notes_url": "release.notes.url",
+        }
+        test_case_actual = {
+            **test_validate_version_base_options,
+        }
+        if major_version is not None:
+            test_case_actual["major_version"] = major_version
+        if minor_version is not None:
+            test_case_actual["minor_version"] = minor_version
+
+        test_case_expected = {
+            **test_validate_version_base_options,
+            "major_version": str(asserted_major_version),
+            "minor_version": str(asserted_minor_version),
+        }
+
+        if is_negative:
+            return test_case_actual
+        return test_case_actual, test_case_expected
+
+    # Generating Positive Test Cases for test_validate_versions
+    test_positive_options = [
+        generate_valid_version_options("1", "2", "1", "2"),
+        generate_valid_version_options("2", "1", "2", "1"),
+        generate_valid_version_options(None, "2", "1", "2"),
+        generate_valid_version_options("1", None, "1", "2"),
+        generate_valid_version_options("2", None, "2", "0"),
+        generate_valid_version_options(None, None, "1", "2"),
+    ]
+
+    @pytest.mark.parametrize("actual_options,expected_options", test_positive_options)
+    def test_positive_validate_versions(self, actual_options, expected_options):
+        """Runs Postive Tests for tests_validate_versions"""
+        task = create_task(PackageUpload, actual_options)
+        task._get_one_record = mock.Mock(
+            return_value={
+                "MajorVersion": 1,
+                "MinorVersion": 1,
+                "PatchVersion": 0,
+                "ReleaseState": "Released",
+            }
+        )
+        task._validate_versions()
+
+        assert task.options["name"] == expected_options["name"]
+        assert task.options["production"] == expected_options["production"]
+        assert task.options["password"] == expected_options["password"]
+        assert task.options["post_install_url"] == expected_options["post_install_url"]
+        assert (
+            task.options["release_notes_url"] == expected_options["release_notes_url"]
+        )
+        assert task.options["major_version"] == expected_options["major_version"]
+        assert task.options["minor_version"] == expected_options["minor_version"]
+
+    def test_positive_validate_versions_for_beta(self):
+        actual_options = {
+            "name": "Test Release",
+            "production": False,
+            "description": "Test Description",
+            "password": "secret",
+            "post_install_url": "post.install.url",
+            "release_notes_url": "release.notes.url",
+            "major_version": "1",
+        }
+        expected_options = {
+            "name": "Test Release",
+            "production": False,
+            "description": "Test Description",
+            "password": "secret",
+            "post_install_url": "post.install.url",
+            "release_notes_url": "release.notes.url",
+            "major_version": "1",
+            "minor_version": "1",
+        }
+        task = create_task(PackageUpload, actual_options)
+        task._get_one_record = mock.Mock(
+            return_value={
+                "MajorVersion": 1,
+                "MinorVersion": 1,
+                "PatchVersion": 0,
+                "ReleaseState": "Beta",
+            }
+        )
+        task._validate_versions()
+
+        assert task.options["name"] == expected_options["name"]
+        assert task.options["production"] == expected_options["production"]
+        assert task.options["password"] == expected_options["password"]
+        assert task.options["post_install_url"] == expected_options["post_install_url"]
+        assert (
+            task.options["release_notes_url"] == expected_options["release_notes_url"]
+        )
+        assert task.options["major_version"] == expected_options["major_version"]
+        assert task.options["minor_version"] == expected_options["minor_version"]
+
+    # Generating Negative Test Cases for test_validate_versions
+    test_negative_options = [
+        generate_valid_version_options("0", "2", None, None, True),
+        generate_valid_version_options("1", "0", None, None, True),
+        generate_valid_version_options(None, "1", None, None, True),
+        generate_valid_version_options("ab", 0, None, None, True),
+    ]
+
+    @pytest.mark.parametrize("actual_options", test_negative_options)
+    def test_negative_validate_versions(self, actual_options):
+        """Running Negative Tests for tests_validate_versions"""
+        task = create_task(PackageUpload, actual_options)
+        task._get_one_record = mock.Mock(
+            return_value={
+                "MajorVersion": 1,
+                "MinorVersion": 1,
+                "PatchVersion": 0,
+                "ReleaseState": "Released",
+            }
+        )
+        with pytest.raises(TaskOptionsError):
+            task._validate_versions()
+
     def test_set_package_info(self):
         expected_package_id = "12345"
         options = {
@@ -84,6 +251,8 @@ class TestPackageUpload:
             "password": "secret",
             "post_install_url": "post.install.url",
             "release_notes_url": "release.notes.url",
+            "major_version": "2",
+            "minor_version": "2",
         }
 
         task = create_task(PackageUpload, options)
@@ -101,6 +270,8 @@ class TestPackageUpload:
         assert options["password"] == task.package_info["Password"]
         assert options["post_install_url"] == task.package_info["PostInstallUrl"]
         assert options["release_notes_url"] == task.package_info["ReleaseNotesUrl"]
+        assert options["major_version"] == task.package_info["MajorVersion"]
+        assert options["minor_version"] == task.package_info["MinorVersion"]
 
     @mock.patch("cumulusci.tasks.salesforce.package_upload.datetime")
     def test_make_package_upload_request(self, datetime):
@@ -238,6 +409,18 @@ class TestPackageUpload:
             task.tooling = mock.Mock(
                 query=mock.Mock(
                     side_effect=[
+                        # Query for latest major and minor version
+                        {
+                            "totalSize": 1,
+                            "records": [
+                                {
+                                    "MajorVersion": 1,
+                                    "MinorVersion": 0,
+                                    "PatchVersion": 1,
+                                    "ReleaseState": "Released",
+                                }
+                            ],
+                        },
                         # Query for package by namespace
                         {"totalSize": 1, "records": [{"Id": "PKG_ID"}]},
                         # Query for upload status