Repository: SFDO-Tooling/CumulusCI
Pull request #3654 (2023-09-20 04:29:19)

Query with install_key in promote_package_version (via @zenibako)
(Via #3567)

If you attempt to use an installation key with the create_package_version and promote_package_version tasks (particularly with the build_unlocked_test_package flow), you will be blocked by a Salesforce SOQL error that demands you include the Installation key in the where clause of the query.

This PR updates these tasks and enables the build_unlocked_test_package flow to work when an installation key is configured for its tasks. It also adds install_key to promote_package_version as an option.


Patch:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index ba6dd2233..6801fd7b6 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -36,4 +36,5 @@ For example:
 
 * Ed Rivas (jerivas)
 * Gustavo Tandeciarz (dcinzona)
+* Chandler Anderson (zenibako)
 * Ben French (BenjaminFrench)
diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml
index 57cd1e022..ab6adc8a5 100644
--- a/cumulusci/cumulusci.yml
+++ b/cumulusci/cumulusci.yml
@@ -1327,6 +1327,7 @@ flows:
                 task: promote_package_version
                 options:
                     version_id: ^^create_package_version.subscriber_package_version_id
+                    install_key: ^^create_package_version.install_key
     unmanaged_ee:
         group: Deployment
         description: Deploy the unmanaged package metadata and all dependencies to the target EE org
diff --git a/cumulusci/tasks/create_package_version.py b/cumulusci/tasks/create_package_version.py
index c9a08ca72..ac958df95 100644
--- a/cumulusci/tasks/create_package_version.py
+++ b/cumulusci/tasks/create_package_version.py
@@ -37,6 +37,9 @@ from cumulusci.salesforce_api.utils import get_simple_salesforce_connection
 from cumulusci.tasks.salesforce.BaseSalesforceApiTask import BaseSalesforceApiTask
 from cumulusci.tasks.salesforce.org_settings import build_settings_package
 from cumulusci.utils.git import split_repo_url
+from cumulusci.utils.salesforce.soql import (
+    format_subscriber_package_version_where_clause,
+)
 
 
 class PackageTypeEnum(StrEnum):
@@ -251,9 +254,12 @@ class CreatePackageVersion(BaseSalesforceApiTask):
         ).format()
 
         # get the new version's dependencies from SubscriberPackageVersion
+        where_clause = format_subscriber_package_version_where_clause(
+            package2_version["SubscriberPackageVersionId"],
+            self.options.get("install_key"),
+        )
         res = self.tooling.query(
-            "SELECT Dependencies FROM SubscriberPackageVersion "
-            f"WHERE Id='{package2_version['SubscriberPackageVersionId']}'"
+            "SELECT Dependencies FROM SubscriberPackageVersion " f"WHERE {where_clause}"
         )
         self.return_values["dependencies"] = self._prepare_cci_dependencies(
             res["records"][0]["Dependencies"]
@@ -338,7 +344,6 @@ class CreatePackageVersion(BaseSalesforceApiTask):
         version_bytes = io.BytesIO()
         version_info = zipfile.ZipFile(version_bytes, "w", zipfile.ZIP_DEFLATED)
         try:
-
             # Add the package.zip
             package_hash = package_zip_builder.as_hash()
             version_info.writestr("package.zip", package_zip_builder.as_bytes())
@@ -441,8 +446,11 @@ class CreatePackageVersion(BaseSalesforceApiTask):
             "VersionInfo": version_info,
             "CalculateCodeCoverage": not skip_validation,
         }
-        if "install_key" in self.options:
-            request["InstallKey"] = self.options["install_key"]
+
+        install_key = self.options.get("install_key")
+        if install_key:
+            request["InstallKey"] = install_key
+            self.return_values["install_key"] = install_key
 
         self.logger.info(
             f"Requesting creation of package version {version_number.format()} "
diff --git a/cumulusci/tasks/salesforce/promote_package_version.py b/cumulusci/tasks/salesforce/promote_package_version.py
index f040f2792..f17ca195a 100644
--- a/cumulusci/tasks/salesforce/promote_package_version.py
+++ b/cumulusci/tasks/salesforce/promote_package_version.py
@@ -9,6 +9,9 @@ from cumulusci.core.github import get_version_id_from_tag
 from cumulusci.salesforce_api.utils import get_simple_salesforce_connection
 from cumulusci.tasks.create_package_version import PackageVersionNumber
 from cumulusci.tasks.salesforce import BaseSalesforceApiTask
+from cumulusci.utils.salesforce.soql import (
+    format_subscriber_package_version_where_clause,
+)
 
 
 class PromotePackageVersion(BaseSalesforceApiTask):
@@ -39,6 +42,9 @@ class PromotePackageVersion(BaseSalesforceApiTask):
             ),
             "required": False,
         },
+        "install_key": {
+            "description": "Install key for package. Default is no install key."
+        },
     }
 
     # We do use a Salesforce org, but it's the dev hub obtained using get_devhub_config,
@@ -294,6 +300,7 @@ class PromotePackageVersion(BaseSalesforceApiTask):
 
     def _query_SubscriberPackageVersion(self, spv_id: str) -> Optional[Dict]:
         """Queries for a SubscriberPackageVersion record with the given SubscriberPackageVersionId"""
+
         return self._query_one_tooling(
             [
                 "Id",
@@ -302,7 +309,9 @@ class PromotePackageVersion(BaseSalesforceApiTask):
                 "SubscriberPackageId",
             ],
             "SubscriberPackageVersion",
-            where_clause=f"Id='{spv_id}'",
+            where_clause=format_subscriber_package_version_where_clause(
+                spv_id, self.options.get("install_key")
+            ),
             raise_error=True,
         )
 
diff --git a/cumulusci/utils/salesforce/soql.py b/cumulusci/utils/salesforce/soql.py
new file mode 100644
index 000000000..1152ebedb
--- /dev/null
+++ b/cumulusci/utils/salesforce/soql.py
@@ -0,0 +1,17 @@
+from typing import Optional
+
+
+def format_subscriber_package_version_where_clause(
+    spv_id: str, install_key: Optional[str] = None
+) -> str:
+    """Get the where clause for a SubscriberPackageVersion query
+
+    Does not include the WHERE.
+    Includes the installation key if provided.
+    """
+    where_clause = f"Id='{spv_id}'"
+
+    if install_key:
+        where_clause += f" AND InstallationKey ='{install_key}'"
+
+    return where_clause


Test patch:
diff --git a/cumulusci/tasks/tests/test_create_package_version.py b/cumulusci/tasks/tests/test_create_package_version.py
index a870c4c19..df901a35c 100644
--- a/cumulusci/tasks/tests/test_create_package_version.py
+++ b/cumulusci/tasks/tests/test_create_package_version.py
@@ -274,7 +274,8 @@ class TestCreatePackageVersion:
                 ],
             },
         )
-        responses.add(  # query dependency org for installed package 1)
+        # query dependency org for installed package 1
+        responses.add(
             "GET",
             f"{self.scratch_base_url}/tooling/query/",
             json={
@@ -290,7 +291,7 @@ class TestCreatePackageVersion:
                     }
                 ],
             },
-        ),
+        )
         responses.add(  # query dependency org for installed package 2)
             "GET",
             f"{self.scratch_base_url}/tooling/query/",
@@ -459,6 +460,7 @@ class TestCreatePackageVersion:
         assert task.return_values["dependencies"] == [
             {"version_id": "04t000000000009AAA"}
         ]
+        assert task.return_values["install_key"] == task.options["install_key"]
         zf.close()
 
     @responses.activate
diff --git a/cumulusci/tasks/tests/test_promote_package_version.py b/cumulusci/tasks/tests/test_promote_package_version.py
index 71ef95d12..c6fe1014b 100644
--- a/cumulusci/tasks/tests/test_promote_package_version.py
+++ b/cumulusci/tasks/tests/test_promote_package_version.py
@@ -1,4 +1,5 @@
 import logging
+from typing import Optional
 from unittest import mock
 
 import pytest
@@ -62,22 +63,24 @@ class TestPromotePackageVersion(GithubApiTestMixin):
         f"https://devhub.my.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}"
     )
 
-    def _mock_target_package_api_calls(self):
+    def _mock_target_package_api_calls(self, install_key: Optional[str] = None):
+        record = {
+            "BuildNumber": 0,
+            "Id": "main_package",
+            "IsReleased": False,
+            "MajorVersion": 1,
+            "MinorVersion": 0,
+            "PatchVersion": 0,
+        }
+        if install_key:
+            record["InstallKey"] = install_key
+
         responses.add(  # query for main package's Package2Version info
             "GET",
             f"{self.devhub_base_url}/tooling/query/",
             json={
                 "size": 1,
-                "records": [
-                    {
-                        "BuildNumber": 0,
-                        "Id": "main_package",
-                        "IsReleased": False,
-                        "MajorVersion": 1,
-                        "MinorVersion": 0,
-                        "PatchVersion": 0,
-                    }
-                ],
+                "records": [record],
                 "done": True,
             },
         )
@@ -188,6 +191,25 @@ class TestPromotePackageVersion(GithubApiTestMixin):
         ):
             task()
 
+    @responses.activate
+    def test_run_task__install_key(self, task, devhub_config):
+        # 20 dependencies, 10 are 2GP, 5 of those are not yet promoted
+        task.options["install_key"] = "hunter2"
+        self._mock_dependencies(20, 10, 5)
+        self._mock_target_package_api_calls(install_key="hunter2")
+        with mock.patch(
+            "cumulusci.tasks.salesforce.promote_package_version.get_devhub_config",
+            return_value=devhub_config,
+        ):
+            with mock.patch(
+                "cumulusci.tasks.salesforce.promote_package_version.get_simple_salesforce_connection",
+            ) as get_connection:
+                task()
+                assert (
+                    "hunter2"
+                    in get_connection.return_value.query_all.call_args_list[0][0][0]
+                )
+
     @responses.activate
     def test_run_task__no_dependencies(self, task, devhub_config):
         self._mock_dependencies(0, 0, 0)
diff --git a/cumulusci/utils/salesforce/tests/test_soql.py b/cumulusci/utils/salesforce/tests/test_soql.py
new file mode 100644
index 000000000..a68419f60
--- /dev/null
+++ b/cumulusci/utils/salesforce/tests/test_soql.py
@@ -0,0 +1,30 @@
+import pytest
+
+from cumulusci.utils.salesforce.soql import (
+    format_subscriber_package_version_where_clause,
+)
+
+
+class TestSoql:
+    spv_id = "04t000000000000"
+
+    @pytest.mark.vcr()
+    def test_format_subscriber_package_version_where_clause_simple(self):
+        where_clause = format_subscriber_package_version_where_clause(self.spv_id, None)
+        assert f"Id='{self.spv_id}'" in where_clause
+        assert " AND InstallationKey =" not in where_clause
+
+    @pytest.mark.vcr()
+    def format_subscriber_package_version_where_clause_install_key_set(self):
+        install_key = "hunter2"
+        where_clause = format_subscriber_package_version_where_clause(
+            self.spv_id, install_key
+        )
+        assert f"Id='{self.spv_id}'" in where_clause
+        assert f" AND InstallationKey ='{install_key}'" in where_clause
+
+    @pytest.mark.vcr()
+    def format_subscriber_package_version_where_clause_install_key_none(self):
+        where_clause = format_subscriber_package_version_where_clause(self.spv_id, None)
+        assert f"Id='{self.spv_id}'" in where_clause
+        assert " AND InstallationKey =" not in where_clause