Repository: SFDO-Tooling/CumulusCI
Pull request #3296 (2022-07-20 16:52:05)

Omnistudio Retrieve/Deploy Tasks
* CumulusCI has a new task `vlocity_pack_deploy`. This task wraps the `vlocity` CLI tool and executes a `packDeploy` command against an org.
* CumulusCI has a new task `vlocity_pack_export`. This task wraps the `vlocity` CLI tool and executes a `packExport` command against an org.


Patch:
diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml
index 15a4c047d..a15879fda 100644
--- a/cumulusci/cumulusci.yml
+++ b/cumulusci/cumulusci.yml
@@ -676,6 +676,14 @@ tasks:
         group: NPSP/EDA
         options:
             restore: True
+    vlocity_pack_export:
+        class_path: cumulusci.tasks.vlocity.vlocity.VlocityRetrieveTask
+        description: "Executes the `vlocity packExport` command against an org"
+        group: Vlocity
+    vlocity_pack_deploy:
+        class_path: cumulusci.tasks.vlocity.vlocity.VlocityDeployTask
+        description: "Executes the `vlocity packDeploy` command against an org"
+        group: Vlocity
 flows:
     ci_beta:
         group: Continuous Integration
diff --git a/cumulusci/tasks/vlocity/exceptions.py b/cumulusci/tasks/vlocity/exceptions.py
new file mode 100644
index 000000000..ccd328073
--- /dev/null
+++ b/cumulusci/tasks/vlocity/exceptions.py
@@ -0,0 +1,2 @@
+class BuildToolMissingError(Exception):
+    pass
diff --git a/cumulusci/tasks/vlocity/vlocity.py b/cumulusci/tasks/vlocity/vlocity.py
new file mode 100644
index 000000000..67d821472
--- /dev/null
+++ b/cumulusci/tasks/vlocity/vlocity.py
@@ -0,0 +1,86 @@
+from abc import ABC
+from typing import Final
+
+import sarge
+
+from cumulusci.core.config.scratch_org_config import ScratchOrgConfig
+from cumulusci.core.tasks import BaseSalesforceTask
+from cumulusci.tasks.command import Command
+from cumulusci.tasks.vlocity.exceptions import BuildToolMissingError
+
+CLI_KEYWORD = "vlocity"
+BUILD_TOOL_MISSING_ERROR = (
+    "This task requires the Vlocity Build CLI tool which is not currently installed on this system. "
+    "For information on installing this tool visit: https://github.com/vlocityinc/vlocity_build#vlocity-build"
+)
+
+
+class VlocityBaseTask(Command, BaseSalesforceTask):
+    """Call the vlocity build tool cli with params"""
+
+    task_options: dict = {
+        "command": {
+            "description": "The full command to run with the sfdx cli.",
+            "required": True,
+        },
+        "extra": {"description": "Any extra arguments to pass to the vlocity CLI"},
+    }
+
+    def _init_task(self):
+        tool_exists = self._vlocity_build_tool_exists()
+        if not tool_exists:
+            raise BuildToolMissingError(BUILD_TOOL_MISSING_ERROR)
+
+    def _vlocity_build_tool_exists(self) -> bool:
+        try:
+            p = sarge.Command("vlocity", stdout=sarge.Capture(buffer_size=-1))
+            p.run(async_=True)
+            p.wait()
+        except ValueError:
+            return False
+        else:
+            return True
+
+    def _get_command(self) -> str:
+        command: str = f"{CLI_KEYWORD} {self.options['command']}"
+
+        if extra_options := self.options.get("extra"):
+            command += f" {extra_options}"
+        return command
+
+
+class VlocitySimpleJobTask(VlocityBaseTask, ABC):
+    """Abstract class for working with the `vlocity` CLI tool"""
+
+    task_options: dict = {
+        "job_file": {"description": "Filepath to the jobfile", "required": True},
+        "extra": {"description": "Any extra arguments to pass to the vlocity CLI"},
+    }
+
+    def _get_command(self) -> str:
+        username: str = self.org_config.username
+        job_file: str = self.options.get("job_file")
+
+        command: str = f"{self.command_keyword} -job {job_file} --json"
+
+        if isinstance(self.org_config, ScratchOrgConfig):
+            command = f"{command} -sfdx.username '{username}'"
+        else:
+            access_token: str = f"-sf.accessToken '{self.org_config.access_token}'"
+            instance_url: str = f"-sf.instanceUrl '{self.org_config.instance_url}'"
+            command = f"{command} {access_token} {instance_url}"
+
+        self.options["command"] = command
+        return super()._get_command()
+
+
+class VlocityRetrieveTask(VlocitySimpleJobTask):
+    """Runs a `vlocity packExport` command with a given user and job file"""
+
+    command_keyword: Final[str] = "packExport"
+
+
+class VlocityDeployTask(VlocitySimpleJobTask):
+    """Runs a `vlocity packDeploy` command with a given user and job file"""
+
+    command_keyword: Final[str] = "packDeploy"


Test patch:
diff --git a/cumulusci/tasks/vlocity/tests/test_vlocity.py b/cumulusci/tasks/vlocity/tests/test_vlocity.py
new file mode 100644
index 000000000..657c51f7d
--- /dev/null
+++ b/cumulusci/tasks/vlocity/tests/test_vlocity.py
@@ -0,0 +1,103 @@
+from unittest import mock
+
+import pytest
+
+from cumulusci.core.config import TaskConfig
+from cumulusci.core.config.org_config import OrgConfig
+from cumulusci.core.config.scratch_org_config import ScratchOrgConfig
+from cumulusci.tasks.vlocity.exceptions import BuildToolMissingError
+from cumulusci.tasks.vlocity.vlocity import (
+    BUILD_TOOL_MISSING_ERROR,
+    VlocityDeployTask,
+    VlocityRetrieveTask,
+)
+
+username = "foo"
+org_name = "dev"
+access_token = "foo.bar.baz"
+instance_url = "https://something.custom.salesforce.com"
+scratch_org_config = ScratchOrgConfig(
+    {
+        "instance_url": "https://test.salesforce.com",
+        "username": username,
+        "org_id": "00Dxxxxxxxxxxxx",
+        "password": "test",
+    },
+    org_name,
+    keychain=mock.Mock(),
+)
+persistent_org_config = OrgConfig(
+    {
+        "instance_url": instance_url,
+        "username": username,
+        "org_id": "00Dxxxxxxxxxxxx",
+        "access_token": access_token,
+    },
+    org_name,
+    keychain=mock.Mock(),
+)
+
+
+vlocity_test_cases = [
+    (
+        scratch_org_config,
+        VlocityRetrieveTask,
+        None,
+        f"vlocity packExport -job vlocity.yaml --json -sfdx.username '{username}'",
+    ),
+    (
+        persistent_org_config,
+        VlocityRetrieveTask,
+        None,
+        f"vlocity packExport -job vlocity.yaml --json -sf.accessToken '{access_token}' -sf.instanceUrl '{instance_url}'",
+    ),
+    (
+        scratch_org_config,
+        VlocityDeployTask,
+        None,
+        f"vlocity packDeploy -job vlocity.yaml --json -sfdx.username '{username}'",
+    ),
+    (
+        persistent_org_config,
+        VlocityDeployTask,
+        None,
+        f"vlocity packDeploy -job vlocity.yaml --json -sf.accessToken '{access_token}' -sf.instanceUrl '{instance_url}'",
+    ),
+    (
+        persistent_org_config,
+        VlocityDeployTask,
+        "foo=bar",
+        f"vlocity packDeploy -job vlocity.yaml --json -sf.accessToken '{access_token}' -sf.instanceUrl '{instance_url}' foo=bar",
+    ),
+]
+
+
+@pytest.mark.parametrize(
+    "org_config,task_class,extra,expected_command", vlocity_test_cases
+)
+def test_vlocity_simple_job(
+    project_config, org_config, task_class, extra, expected_command
+):
+
+    task_config = TaskConfig(
+        config={
+            "options": {"job_file": "vlocity.yaml", "org": org_name, "extra": extra}
+        }
+    )
+    task = task_class(project_config, task_config, org_config)
+
+    assert task._get_command() == expected_command
+
+
+def test_vlocity_build_tool_missing(project_config):
+    task_config = TaskConfig(
+        config={"options": {"job_file": "vlocity.yaml", "org": org_name}}
+    )
+    task = VlocityRetrieveTask(project_config, task_config, scratch_org_config)
+
+    with mock.patch(
+        "cumulusci.tasks.vlocity.vlocity.sarge.Command",
+        mock.Mock(side_effect=ValueError),
+    ):
+        with pytest.raises(BuildToolMissingError, match=BUILD_TOOL_MISSING_ERROR):
+            task._init_task()