Repository: SFDO-Tooling/CumulusCI
Pull request #3665 (2023-09-25 10:21:21)

Implemented variable substitution for nested structures in cumulusci.yml
[W-14176826](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00001bC27HYAS/view)

Implemented functionality to support variable replacement in nested structures in the `cumulusci.yml` file such as:
```
task:
    deploy:
        options:
            path: $project_config.project__package__path
            transforms:
                - transform: find_replace
                  options:
                      patterns:
                          - find: foo
                            replace: $project_config.project__package__namespace
``` 
Here both variables are substituted by the appropriate value. Previously only the `path` would've been substituted


Patch:
diff --git a/cumulusci/core/tasks.py b/cumulusci/core/tasks.py
index c2e95c9db..e9f5b2f11 100644
--- a/cumulusci/core/tasks.py
+++ b/cumulusci/core/tasks.py
@@ -135,13 +135,26 @@ class BaseTask:
             self.options.update(kwargs)
 
         # Handle dynamic lookup of project_config values via $project_config.attr
-        for option, value in self.options.items():
-            if isinstance(value, str):
-                value = PROJECT_CONFIG_RE.sub(
+        def process_options(option):
+            if isinstance(option, str):
+                return PROJECT_CONFIG_RE.sub(
                     lambda match: str(self.project_config.lookup(match.group(1), None)),
-                    value,
+                    option,
                 )
-                self.options[option] = value
+            elif isinstance(option, dict):
+                processed_dict = {}
+                for key, value in option.items():
+                    processed_dict[key] = process_options(value)
+                return processed_dict
+            elif isinstance(option, list):
+                processed_list = []
+                for item in option:
+                    processed_list.append(process_options(item))
+                return processed_list
+            else:
+                return option
+
+        self.options = process_options(self.options)
 
         if self.Options:
             try:


Test patch:
diff --git a/cumulusci/core/tests/test_tasks.py b/cumulusci/core/tests/test_tasks.py
index 579c1bfb0..38535a8c4 100644
--- a/cumulusci/core/tests/test_tasks.py
+++ b/cumulusci/core/tests/test_tasks.py
@@ -105,6 +105,22 @@ class TestBaseTaskCallable:
         task = BaseTask(self.project_config, self.task_config, self.org_config)
         assert task.options["test_option"] == "baz"
 
+    # For variable substitution in nested structures
+    def test_init_options__project_config_substitution_nested(self):
+        self.project_config.config["foo"] = {"bar": "baz", "fighters": "pretender"}
+        self.project_config.config["vulf"] = {"peck": "DeanTown"}
+        self.task_config.config["options"] = {
+            "test_option": "$project_config.foo__bar",
+            "songs": [
+                {"foo_fighters": "$project_config.foo__fighters"},
+                {"vulfpeck": "$project_config.vulf__peck"},
+            ],
+        }
+        task = BaseTask(self.project_config, self.task_config, self.org_config)
+        assert task.options["test_option"] == "baz"
+        assert task.options["songs"][0]["foo_fighters"] == "pretender"
+        assert task.options["songs"][1]["vulfpeck"] == "DeanTown"
+
     def test_init_options__not_shared(self):
         self.project_config.config["foo"] = {"bar": "baz"}
         self.task_config.config["options"] = {}