From cd43818d827d6ceca61d1dfdbe57fefabd0ff502 Mon Sep 17 00:00:00 2001 From: Bharath Chadarajupalli Date: Thu, 29 May 2025 23:43:08 -0700 Subject: [PATCH] Adding an out-dir option to retreive changes --- cumulusci/tasks/salesforce/sourcetracking.py | 22 ++++++- .../salesforce/tests/test_sourcetracking.py | 63 +++++++++++++++++-- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/cumulusci/tasks/salesforce/sourcetracking.py b/cumulusci/tasks/salesforce/sourcetracking.py index 27567ddb20..f58a52e651 100644 --- a/cumulusci/tasks/salesforce/sourcetracking.py +++ b/cumulusci/tasks/salesforce/sourcetracking.py @@ -179,6 +179,14 @@ def _reset_sfdx_snapshot(self): "namespace_tokenize" ] +retrieve_changes_task_options["output_dir"] = { + "description": ( + "The output directory for the retrieved metadata. " + + "If not specified, defaults to force-app or the target directory passed to retrieve changes." + ), + "required": False, +} + def _write_manifest(changes, path, api_version): """Write a package.xml for the specified changes and API version.""" @@ -226,6 +234,7 @@ def retrieve_components( project_config: BaseProjectConfig = None, retrieve_complete_profile: bool = False, capture_output: bool = False, + output_dir: str = None, ): """Retrieve specified components from an org into a target folder. @@ -238,7 +247,10 @@ def retrieve_components( to a namespace prefix to replace it with a `%%%NAMESPACE%%%` token. """ - target = os.path.realpath(target) + # Always use output_dir if specified, else use target + retrieve_target = ( + os.path.realpath(output_dir) if output_dir else os.path.realpath(target) + ) profiles = [] # If retrieve_complete_profile and project_config is None, raise error # This is because project_config is only required if retrieve_complete_profile is True @@ -302,6 +314,8 @@ def retrieve_components( "-w", "5", "--ignore-conflicts", + "--output-dir", + retrieve_target, ], capture_output=capture_output, check_return=True, @@ -364,6 +378,9 @@ def _init_options(self, kwargs): self.options.get("retrieve_complete_profile", False) ) + # Get output_dir first + output_dir = self.options.get("output_dir") + # Check which directories are configured as dx packages package_directories = [] default_package_directory = None @@ -392,6 +409,7 @@ def _init_options(self, kwargs): md_format = path not in package_directories self.md_format = md_format self.options["path"] = path + self.options["output_dir"] = output_dir if "api_version" not in self.options: self.options[ @@ -410,6 +428,7 @@ def _run_task(self): self.logger.info("{MemberType}: {MemberName}".format(**change)) target = os.path.realpath(self.options["path"]) + output_dir = self.options.get("output_dir") package_xml_opts = {} if self.options["path"] == "src": package_xml_opts.update( @@ -430,6 +449,7 @@ def _run_task(self): extra_package_xml_opts=package_xml_opts, project_config=self.project_config, retrieve_complete_profile=self.options["retrieve_complete_profile"], + output_dir=output_dir, ) if self.options["snapshot"]: diff --git a/cumulusci/tasks/salesforce/tests/test_sourcetracking.py b/cumulusci/tasks/salesforce/tests/test_sourcetracking.py index 258d1a1e32..206b344970 100644 --- a/cumulusci/tasks/salesforce/tests/test_sourcetracking.py +++ b/cumulusci/tasks/salesforce/tests/test_sourcetracking.py @@ -20,6 +20,11 @@ from cumulusci.utils import temporary_dir +@pytest.fixture +def vcr_cassette_dir(): + return os.path.join(os.path.dirname(__file__), "cassettes") + + class TestListChanges: """List the changes from a scratch org""" @@ -180,12 +185,12 @@ def test_run_task(self, sfdx, create_task_fixture): }, ], } - with mock.patch.object( - RetrieveProfile, "_run_task" - ) as mock_retrieve_profile, mock.patch.object( - pathlib.Path, "exists", return_value=True - ), mock.patch.object( - pathlib.Path, "is_dir", return_value=True + with ( + mock.patch.object( + RetrieveProfile, "_run_task" + ) as mock_retrieve_profile, + mock.patch.object(pathlib.Path, "exists", return_value=True), + mock.patch.object(pathlib.Path, "is_dir", return_value=True), ): task._run_task() assert sfdx_calls == [ @@ -208,6 +213,52 @@ def test_run_task__no_changes(self, sfdx, create_task_fixture): task._run_task() assert "No changes to retrieve" in messages + def test_run_task_with_output_dir(self, sfdx, create_task_fixture): + sfdx_calls = [] + sfdx.side_effect = lambda cmd, *args, **kw: sfdx_calls.append(cmd) + + with temporary_dir(): + task = create_task_fixture( + RetrieveChanges, + { + "include": "Test", + "namespace_tokenize": "ns", + "retrieve_complete_profile": True, + "output_dir": "output", + }, + ) + task._init_task() + task.tooling = mock.Mock() + task.tooling.query_all.return_value = { + "totalSize": 1, + "records": [ + { + "MemberType": "CustomObject", + "MemberName": "Test__c", + "RevisionCounter": 1, + }, + { + "MemberType": "Profile", + "MemberName": "TestProfile", + "RevisionCounter": 1, + }, + ], + } + with ( + mock.patch.object( + RetrieveProfile, "_run_task" + ) as mock_retrieve_profile, + mock.patch.object(pathlib.Path, "exists", return_value=True), + mock.patch.object(pathlib.Path, "is_dir", return_value=True), + ): + task._run_task() + assert sfdx_calls == [ + "project convert mdapi", + "project retrieve start", + "project convert source", + ] + mock_retrieve_profile.assert_called() + class TestSnapshotChanges: @mock.patch("cumulusci.tasks.salesforce.sourcetracking.sfdx")