cumulusci-plus 5.0.21__py3-none-any.whl → 5.0.43__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. cumulusci/__about__.py +1 -1
  2. cumulusci/cli/logger.py +2 -2
  3. cumulusci/cli/service.py +20 -0
  4. cumulusci/cli/task.py +19 -3
  5. cumulusci/cli/tests/test_error.py +3 -1
  6. cumulusci/cli/tests/test_flow.py +279 -2
  7. cumulusci/cli/tests/test_org.py +5 -0
  8. cumulusci/cli/tests/test_service.py +15 -12
  9. cumulusci/cli/tests/test_task.py +122 -2
  10. cumulusci/cli/tests/utils.py +1 -4
  11. cumulusci/core/config/__init__.py +1 -0
  12. cumulusci/core/config/base_task_flow_config.py +26 -1
  13. cumulusci/core/config/org_config.py +2 -1
  14. cumulusci/core/config/project_config.py +14 -20
  15. cumulusci/core/config/scratch_org_config.py +12 -0
  16. cumulusci/core/config/tests/test_config.py +1 -0
  17. cumulusci/core/config/tests/test_config_expensive.py +9 -3
  18. cumulusci/core/config/universal_config.py +3 -4
  19. cumulusci/core/dependencies/base.py +5 -1
  20. cumulusci/core/dependencies/dependencies.py +1 -1
  21. cumulusci/core/dependencies/github.py +1 -2
  22. cumulusci/core/dependencies/resolvers.py +1 -1
  23. cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
  24. cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
  25. cumulusci/core/flowrunner.py +90 -6
  26. cumulusci/core/github.py +1 -1
  27. cumulusci/core/sfdx.py +3 -1
  28. cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
  29. cumulusci/core/source_transforms/transforms.py +1 -1
  30. cumulusci/core/tasks.py +13 -2
  31. cumulusci/core/tests/test_flowrunner.py +100 -0
  32. cumulusci/core/tests/test_tasks.py +65 -0
  33. cumulusci/core/utils.py +3 -1
  34. cumulusci/core/versions.py +1 -1
  35. cumulusci/cumulusci.yml +73 -1
  36. cumulusci/oauth/client.py +1 -1
  37. cumulusci/plugins/plugin_base.py +5 -3
  38. cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
  39. cumulusci/salesforce_api/rest_deploy.py +1 -1
  40. cumulusci/schema/cumulusci.jsonschema.json +69 -0
  41. cumulusci/tasks/apex/anon.py +1 -1
  42. cumulusci/tasks/apex/testrunner.py +421 -144
  43. cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
  44. cumulusci/tasks/bulkdata/extract.py +0 -1
  45. cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
  46. cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
  47. cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
  48. cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
  49. cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
  50. cumulusci/tasks/bulkdata/select_utils.py +1 -1
  51. cumulusci/tasks/bulkdata/snowfakery.py +100 -25
  52. cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
  53. cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
  54. cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
  55. cumulusci/tasks/bulkdata/tests/test_select_utils.py +46 -0
  56. cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
  57. cumulusci/tasks/create_package_version.py +190 -16
  58. cumulusci/tasks/datadictionary.py +1 -1
  59. cumulusci/tasks/metadata_etl/__init__.py +2 -0
  60. cumulusci/tasks/metadata_etl/applications.py +256 -0
  61. cumulusci/tasks/metadata_etl/base.py +7 -3
  62. cumulusci/tasks/metadata_etl/layouts.py +1 -1
  63. cumulusci/tasks/metadata_etl/permissions.py +1 -1
  64. cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
  65. cumulusci/tasks/metadata_etl/tests/test_applications.py +710 -0
  66. cumulusci/tasks/push/README.md +15 -17
  67. cumulusci/tasks/release_notes/README.md +13 -13
  68. cumulusci/tasks/release_notes/generator.py +13 -8
  69. cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
  70. cumulusci/tasks/salesforce/Deploy.py +53 -2
  71. cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
  72. cumulusci/tasks/salesforce/__init__.py +1 -0
  73. cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
  74. cumulusci/tasks/salesforce/composite.py +1 -1
  75. cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
  76. cumulusci/tasks/salesforce/enable_prediction.py +5 -1
  77. cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
  78. cumulusci/tasks/salesforce/insert_record.py +18 -19
  79. cumulusci/tasks/salesforce/sourcetracking.py +1 -1
  80. cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
  81. cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
  82. cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
  83. cumulusci/tasks/salesforce/tests/test_enable_prediction.py +4 -2
  84. cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
  85. cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
  86. cumulusci/tasks/salesforce/tests/test_update_external_auth_identity_provider.py +927 -0
  87. cumulusci/tasks/salesforce/tests/test_update_external_credential.py +1427 -0
  88. cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
  89. cumulusci/tasks/salesforce/tests/test_update_record.py +512 -0
  90. cumulusci/tasks/salesforce/update_dependencies.py +2 -2
  91. cumulusci/tasks/salesforce/update_external_auth_identity_provider.py +551 -0
  92. cumulusci/tasks/salesforce/update_external_credential.py +647 -0
  93. cumulusci/tasks/salesforce/update_named_credential.py +441 -0
  94. cumulusci/tasks/salesforce/update_profile.py +17 -13
  95. cumulusci/tasks/salesforce/update_record.py +217 -0
  96. cumulusci/tasks/salesforce/users/permsets.py +62 -5
  97. cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
  98. cumulusci/tasks/sfdmu/__init__.py +0 -0
  99. cumulusci/tasks/sfdmu/sfdmu.py +376 -0
  100. cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
  101. cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
  102. cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
  103. cumulusci/tasks/tests/test_create_package_version.py +716 -1
  104. cumulusci/tasks/tests/test_util.py +42 -0
  105. cumulusci/tasks/util.py +37 -1
  106. cumulusci/tasks/utility/copyContents.py +402 -0
  107. cumulusci/tasks/utility/credentialManager.py +302 -0
  108. cumulusci/tasks/utility/directoryRecreator.py +30 -0
  109. cumulusci/tasks/utility/env_management.py +1 -1
  110. cumulusci/tasks/utility/secretsToEnv.py +135 -0
  111. cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
  112. cumulusci/tasks/utility/tests/test_credentialManager.py +1150 -0
  113. cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
  114. cumulusci/tasks/utility/tests/test_secretsToEnv.py +1118 -0
  115. cumulusci/tests/test_integration_infrastructure.py +3 -1
  116. cumulusci/tests/test_utils.py +70 -6
  117. cumulusci/utils/__init__.py +54 -9
  118. cumulusci/utils/classutils.py +5 -2
  119. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
  120. cumulusci/utils/options.py +23 -1
  121. cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
  122. cumulusci/utils/yaml/cumulusci_yml.py +8 -3
  123. cumulusci/utils/yaml/model_parser.py +2 -2
  124. cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
  125. cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
  126. cumulusci/vcs/base.py +23 -15
  127. cumulusci/vcs/bootstrap.py +5 -4
  128. cumulusci/vcs/utils/list_modified_files.py +189 -0
  129. cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
  130. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/METADATA +11 -10
  131. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/RECORD +135 -104
  132. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/WHEEL +1 -1
  133. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/entry_points.txt +0 -0
  134. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/AUTHORS.rst +0 -0
  135. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/LICENSE +0 -0
@@ -17,10 +17,7 @@ def run_click_command(cmd, *args, **kw):
17
17
 
18
18
  def run_cli_command(*args, runtime=None, input=None, **kw):
19
19
  """Run a click command with arg parsing and injected CCI runtime object."""
20
- if tuple(map(int, click.__version__.split("."))) >= (8, 1, 0):
21
- runner = CliRunner()
22
- else:
23
- runner = CliRunner(mix_stderr=False)
20
+ runner = CliRunner()
24
21
  result = runner.invoke(
25
22
  cli,
26
23
  args,
@@ -68,6 +68,7 @@ class TaskConfig(BaseConfig):
68
68
  name: str
69
69
  checks: list
70
70
  project_config: "BaseProjectConfig"
71
+ is_global: bool
71
72
 
72
73
  # TODO: What if an intermediate repo "allows" a downstream repo?
73
74
  # Only the top repo should be allowed to do so.
@@ -1,5 +1,5 @@
1
1
  from difflib import get_close_matches
2
- from typing import Any, Dict, List
2
+ from typing import Any, Dict, List, cast
3
3
 
4
4
  from cumulusci.core.config import BaseConfig, FlowConfig, TaskConfig
5
5
  from cumulusci.core.exceptions import (
@@ -7,6 +7,8 @@ from cumulusci.core.exceptions import (
7
7
  FlowNotFoundError,
8
8
  TaskNotFoundError,
9
9
  )
10
+ from cumulusci.core.utils import merge_config
11
+ from cumulusci.plugins.plugin_loader import load_plugins
10
12
 
11
13
 
12
14
  def list_infos(infos: dict) -> List[Dict[str, str]]:
@@ -30,6 +32,7 @@ class BaseTaskFlowConfig(BaseConfig):
30
32
 
31
33
  tasks: dict
32
34
  flows: dict
35
+ config_plugins: dict = {}
33
36
 
34
37
  def list_tasks(self) -> List[Dict[str, str]]:
35
38
  """Returns a list of task info dictionaries with keys 'name' and 'description'"""
@@ -80,3 +83,25 @@ class BaseTaskFlowConfig(BaseConfig):
80
83
  return f'. Did you mean "{match_list[0]}"?'
81
84
  else:
82
85
  return ""
86
+
87
+ def _load_plugins_config(self):
88
+ """Loads the plugin configurations"""
89
+ plugins = load_plugins()
90
+ self.config_plugins = {}
91
+ for plugin in plugins:
92
+ if plugin.plugin_project_config:
93
+ self.config_plugins.update(plugin.plugin_project_config)
94
+ plugin.teardown() # clean up the plugin
95
+
96
+ def merge_base_config(self, base_config: dict) -> dict:
97
+ """Merges the base config with the plugin configurations"""
98
+ self._load_plugins_config()
99
+ return cast(
100
+ dict,
101
+ merge_config(
102
+ {
103
+ **base_config,
104
+ "plugins_config": self.config_plugins,
105
+ }
106
+ ),
107
+ )
@@ -63,7 +63,7 @@ class OrgConfig(BaseConfig):
63
63
  client_secret: str
64
64
  connected_app: str
65
65
  serialization_format: str
66
-
66
+ org_name: str
67
67
  createable: Optional[bool] = None
68
68
 
69
69
  # make sure it can be mocked for tests
@@ -246,6 +246,7 @@ class OrgConfig(BaseConfig):
246
246
  "is_sandbox": self._org_sobject["IsSandbox"],
247
247
  "instance_name": self._org_sobject["InstanceName"],
248
248
  "namespace": self._org_sobject["NamespacePrefix"],
249
+ "org_name": self._org_sobject["Name"],
249
250
  }
250
251
  self.config.update(result)
251
252
 
@@ -14,12 +14,11 @@ from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union
14
14
  from cumulusci.core.config.base_config import BaseConfig
15
15
  from cumulusci.core.debug import get_debug_mode
16
16
  from cumulusci.core.versions import PackageVersionNumber
17
- from cumulusci.plugins.plugin_loader import load_plugins
18
17
  from cumulusci.utils.version_strings import LooseVersion
19
18
 
20
19
  API_VERSION_RE = re.compile(r"^\d\d+\.0$")
21
20
 
22
- from pydantic import ValidationError
21
+ from pydantic.v1 import ValidationError
23
22
 
24
23
  from cumulusci.core.config import FlowConfig, TaskConfig
25
24
  from cumulusci.core.config.base_task_flow_config import BaseTaskFlowConfig
@@ -33,7 +32,6 @@ from cumulusci.core.exceptions import (
33
32
  VcsException,
34
33
  )
35
34
  from cumulusci.core.source import LocalFolderSource, NullSource
36
- from cumulusci.core.utils import merge_config
37
35
  from cumulusci.utils.fileutils import FSResource, open_fs_resource
38
36
  from cumulusci.utils.git import current_branch, generic_parse_repo_url, git_path
39
37
  from cumulusci.utils.yaml.cumulusci_yml import (
@@ -84,7 +82,6 @@ class BaseProjectConfig(BaseTaskFlowConfig, ProjectConfigPropertiesMixin):
84
82
  config_project: dict
85
83
  config_project_local: dict
86
84
  config_additional_yaml: dict
87
- config_plugins_yaml: dict
88
85
  additional_yaml: Optional[str]
89
86
  source: Union[NullSource, VCSSource, LocalFolderSource]
90
87
  _cache_dir: Optional[Path]
@@ -114,7 +111,6 @@ class BaseProjectConfig(BaseTaskFlowConfig, ProjectConfigPropertiesMixin):
114
111
  self.config_project = {}
115
112
  self.config_project_local = {}
116
113
  self.config_additional_yaml = {}
117
- self.config_plugins_yaml = {}
118
114
 
119
115
  # optionally pass in a kwarg named 'additional_yaml' that will
120
116
  # be added to the YAML merge stack.
@@ -184,27 +180,13 @@ class BaseProjectConfig(BaseTaskFlowConfig, ProjectConfigPropertiesMixin):
184
180
  if additional_yaml_config:
185
181
  self.config_additional_yaml.update(additional_yaml_config)
186
182
 
187
- # Loading plugins as classes are loaded and available.
188
- plugins = load_plugins()
189
-
190
- # Load the plugin yaml config file if it exists
191
- for plugin in plugins:
192
- if plugin.plugin_project_config:
193
- self.config_plugins_yaml.update(plugin.plugin_project_config)
194
- self.logger.info(
195
- f"Loaded plugin: {plugin.name} ({plugin.api_name}) v{plugin.version}"
196
- )
197
-
198
- plugin.teardown() # clean up the plugin
199
-
200
- self.config = merge_config(
183
+ self.config = self.merge_base_config(
201
184
  {
202
185
  "universal_config": self.config_universal,
203
186
  "global_config": self.config_global,
204
187
  "project_config": self.config_project,
205
188
  "project_local_config": self.config_project_local,
206
189
  "additional_yaml": self.config_additional_yaml,
207
- "plugins_config": self.config_plugins_yaml,
208
190
  }
209
191
  )
210
192
 
@@ -470,6 +452,18 @@ class BaseProjectConfig(BaseTaskFlowConfig, ProjectConfigPropertiesMixin):
470
452
  ):
471
453
  return parts[0]
472
454
 
455
+ @property
456
+ def is_dependency_flow(self) -> bool:
457
+ return (
458
+ False
459
+ if self.executing_dependency_flow is None
460
+ else self.executing_dependency_flow
461
+ )
462
+
463
+ @is_dependency_flow.setter
464
+ def is_dependency_flow(self, value: bool):
465
+ self.executing_dependency_flow = value
466
+
473
467
  def get_repo(self) -> AbstractRepo:
474
468
  repo = self.repo_service.get_repository()
475
469
  if repo is None:
@@ -162,6 +162,18 @@ class ScratchOrgConfig(SfdxOrgConfig):
162
162
  args += ["-a", self.sfdx_alias]
163
163
  with open(self.config_file, "r") as org_def:
164
164
  org_def_data = json.load(org_def)
165
+ if (
166
+ "orgName" in org_def_data
167
+ and org_def_data["orgName"] is not None
168
+ and self.keychain.project_config.project__name is not None
169
+ ):
170
+ org_def_data["orgName"] = (
171
+ org_def_data["orgName"]
172
+ .replace(
173
+ "%%%PROJECT_NAME%%%", self.keychain.project_config.project__name
174
+ )
175
+ .replace("%%%CONFIG_NAME%%%", self.name.upper())
176
+ )
165
177
  org_def_has_email = "adminEmail" in org_def_data
166
178
  if self.email_address and not org_def_has_email:
167
179
  args += [f"--admin-email={self.email_address}"]
@@ -1222,6 +1222,7 @@ class TestOrgConfig:
1222
1222
  "IsSandbox": False,
1223
1223
  "InstanceName": "cs420",
1224
1224
  "NamespacePrefix": "ns",
1225
+ "Name": "Test Org",
1225
1226
  },
1226
1227
  )
1227
1228
 
@@ -54,7 +54,9 @@ class TestUniversalConfig:
54
54
  with open(filename, "w") as f:
55
55
  f.write(content)
56
56
 
57
- def test_load_universal_config_no_local(self, mock_class):
57
+ @mock.patch("cumulusci.core.config.base_task_flow_config.load_plugins")
58
+ def test_load_universal_config_no_local(self, mock_load_plugins, mock_class):
59
+ mock_load_plugins.return_value = []
58
60
  mock_class.return_value = self.tempdir_home
59
61
  # clear cache
60
62
  UniversalConfig.config = None
@@ -63,7 +65,9 @@ class TestUniversalConfig:
63
65
  expected_config = yaml.safe_load(f_expected_config)
64
66
  assert config.config == expected_config
65
67
 
66
- def test_load_universal_config_empty_local(self, mock_class):
68
+ @mock.patch("cumulusci.core.config.base_task_flow_config.load_plugins")
69
+ def test_load_universal_config_empty_local(self, mock_load_plugins, mock_class):
70
+ mock_load_plugins.return_value = []
67
71
  self._create_universal_config_local("")
68
72
  # clear cache
69
73
  UniversalConfig.config = None
@@ -73,7 +77,9 @@ class TestUniversalConfig:
73
77
  expected_config = yaml.safe_load(f_expected_config)
74
78
  assert config.config == expected_config
75
79
 
76
- def test_load_universal_config_with_local(self, mock_class):
80
+ @mock.patch("cumulusci.core.config.base_task_flow_config.load_plugins")
81
+ def test_load_universal_config_with_local(self, mock_load_plugins, mock_class):
82
+ mock_load_plugins.return_value = []
77
83
  local_yaml = "tasks:\n newtesttask:\n description: test description"
78
84
  self._create_universal_config_local(local_yaml)
79
85
  mock_class.return_value = self.tempdir_home
@@ -1,12 +1,12 @@
1
1
  import os
2
2
  from pathlib import Path
3
+ from typing import Optional
3
4
 
4
5
  from cumulusci.core.config import BaseTaskFlowConfig
5
6
  from cumulusci.core.config.project_config import (
6
7
  BaseProjectConfig,
7
8
  ProjectConfigPropertiesMixin,
8
9
  )
9
- from cumulusci.core.utils import merge_config
10
10
  from cumulusci.utils.yaml.cumulusci_yml import cci_safe_load
11
11
 
12
12
  __location__ = os.path.dirname(os.path.realpath(__file__))
@@ -18,10 +18,9 @@ class UniversalConfig(BaseTaskFlowConfig, ProjectConfigPropertiesMixin):
18
18
  project_local_dir: str
19
19
  cli: dict
20
20
 
21
- config = None
21
+ config: Optional[dict] = None
22
22
  config_filename = "cumulusci.yml"
23
23
  project_config_class = BaseProjectConfig
24
- universal_config_obj = None
25
24
 
26
25
  def __init__(self, config=None):
27
26
  self._init_logger()
@@ -80,7 +79,7 @@ class UniversalConfig(BaseTaskFlowConfig, ProjectConfigPropertiesMixin):
80
79
  config = {}
81
80
  UniversalConfig.config_global = config
82
81
 
83
- UniversalConfig.config = merge_config(
82
+ UniversalConfig.config = self.merge_base_config(
84
83
  {
85
84
  "universal_config": UniversalConfig.config_universal,
86
85
  "global_config": UniversalConfig.config_global,
@@ -4,7 +4,7 @@ from datetime import datetime
4
4
  from typing import List, Optional, Type
5
5
  from zipfile import ZipFile
6
6
 
7
- from pydantic import AnyUrl, PrivateAttr, root_validator, validator
7
+ from pydantic.v1 import AnyUrl, PrivateAttr, root_validator, validator
8
8
 
9
9
  from cumulusci.core.config import OrgConfig
10
10
  from cumulusci.core.config.project_config import BaseProjectConfig
@@ -738,11 +738,15 @@ class UnmanagedVcsDependencyFlow(UnmanagedStaticDependency, ABC):
738
738
  )
739
739
  )
740
740
 
741
+ project_config.is_dependency_flow = True
742
+
741
743
  start_time = datetime.now()
742
744
  coordinator.run(org)
743
745
  duration = datetime.now() - start_time
744
746
  context.logger.info(f"Ran {self.flow_name} in {format_duration(duration)}")
745
747
 
748
+ project_config.is_dependency_flow = False
749
+
746
750
 
747
751
  class BasePackageVersionDependency(StaticDependency, ABC):
748
752
  @abstractmethod
@@ -3,7 +3,7 @@ import logging
3
3
  import os
4
4
  from typing import List, Optional, Type
5
5
 
6
- from pydantic import AnyUrl, ValidationError
6
+ from pydantic.v1 import AnyUrl, ValidationError
7
7
 
8
8
  import cumulusci.core.dependencies.base as base_dependency
9
9
  from cumulusci.core.config import OrgConfig
@@ -3,8 +3,7 @@ from abc import ABC
3
3
  from functools import lru_cache
4
4
  from typing import Optional, Type
5
5
 
6
- from pydantic import root_validator
7
- from pydantic.networks import AnyUrl
6
+ from pydantic.v1 import AnyUrl, root_validator
8
7
 
9
8
  import cumulusci.core.dependencies.base as base_dependency
10
9
  from cumulusci.core.exceptions import DependencyResolutionError, GithubApiNotFoundError
@@ -5,7 +5,7 @@ from collections.abc import Mapping
5
5
  from enum import StrEnum
6
6
  from typing import Callable, Iterable, List, Optional, Tuple, Type
7
7
 
8
- from pydantic import AnyUrl
8
+ from pydantic.v1 import AnyUrl
9
9
 
10
10
  from cumulusci.core.config.project_config import BaseProjectConfig
11
11
  from cumulusci.core.dependencies.base import (
@@ -5,7 +5,7 @@ from unittest import mock
5
5
  from zipfile import ZipFile
6
6
 
7
7
  import pytest
8
- from pydantic import ValidationError, root_validator
8
+ from pydantic.v1 import ValidationError, root_validator
9
9
 
10
10
  from cumulusci.core.config.org_config import OrgConfig, VersionInfo
11
11
  from cumulusci.core.config.project_config import BaseProjectConfig
@@ -3,7 +3,7 @@ from unittest import mock
3
3
 
4
4
  import pytest
5
5
  from github3.exceptions import NotFoundError
6
- from pydantic import root_validator
6
+ from pydantic.v1 import root_validator
7
7
 
8
8
  from cumulusci.core.config import UniversalConfig
9
9
  from cumulusci.core.config.project_config import BaseProjectConfig
@@ -53,6 +53,7 @@ Option values/overrides can be passed in at a number of levels, in increasing or
53
53
 
54
54
  import copy
55
55
  import logging
56
+ import os
56
57
  from collections import defaultdict
57
58
  from operator import attrgetter
58
59
  from typing import (
@@ -164,6 +165,11 @@ class StepSpec:
164
165
  )
165
166
 
166
167
 
168
+ class FlowStepSpec(StepSpec):
169
+ def __init__(self, *args, **kwargs):
170
+ super().__init__(*args, **kwargs)
171
+
172
+
167
173
  class StepResult(NamedTuple):
168
174
  step_num: StepVersion
169
175
  task_name: str
@@ -356,6 +362,10 @@ class FlowCoordinator:
356
362
 
357
363
  self.logger = self._init_logger()
358
364
  self.steps = self._init_steps()
365
+ self._expression_cache = {}
366
+ self._jinja2_context = None
367
+ self._context_project_config = None
368
+ self._context_org_config = None
359
369
 
360
370
  @classmethod
361
371
  def from_steps(
@@ -403,6 +413,9 @@ class FlowCoordinator:
403
413
  previous_parts = []
404
414
  previous_source = None
405
415
  for step in self.steps:
416
+ if isinstance(step, FlowStepSpec):
417
+ continue
418
+
406
419
  parts = step.path.split(".")
407
420
  steps = str(step.step_num).split("/")
408
421
  if len(parts) > len(steps):
@@ -491,7 +504,27 @@ class FlowCoordinator:
491
504
  self._rule(new_line=True)
492
505
 
493
506
  try:
507
+ # Pre-evaluate all flow conditions
508
+ skipped_flows_set = set()
494
509
  for step in self.steps:
510
+ if isinstance(step, FlowStepSpec):
511
+ if not self._evaluate_flow_step(step):
512
+ skipped_flows_set.add(step.path)
513
+
514
+ # Main execution loop with optimized path checking
515
+ for step in self.steps:
516
+ if isinstance(step, FlowStepSpec):
517
+ self.logger.info(
518
+ f"Skipping Flow {step.task_name} (skipped unless {step.when})"
519
+ )
520
+ continue
521
+
522
+ if self._is_task_in_skipped_flow(step.path, skipped_flows_set):
523
+ self.logger.info(
524
+ f"Skipping Task {step.task_name} in flow {step.path} (parent flow is skipped)"
525
+ )
526
+ continue
527
+
495
528
  self._run_step(step)
496
529
  flow_name = f"'{self.name}' " if self.name else ""
497
530
  self.logger.info(
@@ -500,6 +533,46 @@ class FlowCoordinator:
500
533
  finally:
501
534
  self.callbacks.post_flow(self)
502
535
 
536
+ def _get_jinja2_context(self, project_config, org_config):
537
+ """Get or create jinja2 context, reusing when possible."""
538
+ if (
539
+ self._jinja2_context is None
540
+ or self._context_project_config != project_config
541
+ or self._context_org_config != org_config
542
+ ):
543
+
544
+ self._jinja2_context = {
545
+ "project_config": project_config,
546
+ "org_config": org_config,
547
+ "env": dict(os.environ),
548
+ }
549
+ self._context_project_config = project_config
550
+ self._context_org_config = org_config
551
+
552
+ return self._jinja2_context
553
+
554
+ def _evaluate_flow_step(self, step: StepSpec) -> bool:
555
+ if not step.when:
556
+ return True
557
+
558
+ # Check cache first
559
+ if step.when in self._expression_cache:
560
+ expr = self._expression_cache[step.when]
561
+ else:
562
+ expr = jinja2_env.compile_expression(step.when)
563
+ self._expression_cache[step.when] = expr
564
+
565
+ jinja2_context = self._get_jinja2_context(step.project_config, self.org_config)
566
+
567
+ return expr(**jinja2_context)
568
+
569
+ def _is_task_in_skipped_flow(self, task_path: str, skipped_flows_set: set) -> bool:
570
+ """Check if task belongs to any skipped flow using O(1) set lookup."""
571
+ for skipped_path in skipped_flows_set:
572
+ if task_path.startswith(skipped_path + "."):
573
+ return True
574
+ return False
575
+
503
576
  def _run_step(self, step: StepSpec):
504
577
  if step.skip:
505
578
  self._rule(fill="*")
@@ -508,12 +581,7 @@ class FlowCoordinator:
508
581
  return
509
582
 
510
583
  if step.when:
511
- jinja2_context = {
512
- "project_config": step.project_config,
513
- "org_config": self.org_config,
514
- }
515
- expr = jinja2_env.compile_expression(step.when)
516
- value = expr(**jinja2_context)
584
+ value = self._evaluate_flow_step(step)
517
585
  if not value:
518
586
  self.logger.info(
519
587
  f"Skipping task {step.task_name} (skipped unless {step.when})"
@@ -681,8 +749,24 @@ class FlowCoordinator:
681
749
  else:
682
750
  path = name
683
751
  step_options = step_config.get("options", {})
752
+ parent_task_options = parent_options.get(name, {})
753
+ step_options.update(parent_task_options)
684
754
  step_ui_options = step_config.get("ui_options", {})
685
755
  flow_config = project_config.get_flow(name)
756
+
757
+ if step_config.get("when"):
758
+ visited_steps.append(
759
+ FlowStepSpec(
760
+ task_config={},
761
+ step_num=step_number,
762
+ task_name=path,
763
+ task_class=None,
764
+ project_config=flow_config.project_config,
765
+ allow_failure=step_config.get("ignore_failure", False),
766
+ when=step_config.get("when"),
767
+ )
768
+ )
769
+
686
770
  for sub_number, sub_stepconf in flow_config.steps.items():
687
771
  # append the flow number to the child number, since its a LooseVersion.
688
772
  # e.g. if we're in step 2.3 which references a flow with steps 1-5, it
cumulusci/core/github.py CHANGED
@@ -189,7 +189,7 @@ def validate_service(options: dict, keychain) -> dict:
189
189
  server_domain = options.get("server_domain", None)
190
190
 
191
191
  gh = _determine_github_client(server_domain, {"token": token})
192
- if type(gh) == GitHubEnterprise:
192
+ if isinstance(gh, GitHubEnterprise):
193
193
  validate_gh_enterprise(server_domain, keychain)
194
194
  try:
195
195
  authed_user = gh.me()
cumulusci/core/sfdx.py CHANGED
@@ -14,10 +14,12 @@ import sarge
14
14
 
15
15
  # Fix for TextIOWrapper flush issue with sarge.Capture objects
16
16
  # Add flush method to sarge.Capture to prevent AttributeError during garbage collection
17
- if not hasattr(sarge.Capture, 'flush'):
17
+ if not hasattr(sarge.Capture, "flush"):
18
+
18
19
  def _capture_flush(self):
19
20
  """No-op flush method for sarge.Capture compatibility with TextIOWrapper"""
20
21
  pass
22
+
21
23
  sarge.Capture.flush = _capture_flush
22
24
 
23
25
  from cumulusci.core.enums import StrEnum
@@ -8,7 +8,7 @@ from zipfile import ZipFile
8
8
 
9
9
  import pytest
10
10
  from lxml import etree as ET
11
- from pydantic import ValidationError
11
+ from pydantic.v1 import ValidationError
12
12
 
13
13
  from cumulusci.core.exceptions import CumulusCIException, TaskOptionsError
14
14
  from cumulusci.core.source_transforms.transforms import (
@@ -10,7 +10,7 @@ from pathlib import Path
10
10
  from zipfile import ZipFile
11
11
 
12
12
  from lxml import etree as ET
13
- from pydantic import BaseModel, root_validator
13
+ from pydantic.v1 import BaseModel, root_validator
14
14
 
15
15
  from cumulusci.core.dependencies.utils import TaskContext
16
16
  from cumulusci.core.enums import StrEnum
cumulusci/core/tasks.py CHANGED
@@ -11,7 +11,7 @@ import time
11
11
  from contextlib import nullcontext
12
12
  from typing import Any, Callable, Dict, List, Optional, Type, Union
13
13
 
14
- from pydantic.error_wrappers import ValidationError
14
+ from pydantic.v1.error_wrappers import ValidationError
15
15
 
16
16
  from cumulusci import __version__
17
17
  from cumulusci.core.config import TaskConfig
@@ -33,6 +33,7 @@ from cumulusci.utils.options import CCIOptions, ReadOnlyOptions
33
33
  CURRENT_TASK = threading.local()
34
34
 
35
35
  PROJECT_CONFIG_RE = re.compile(r"\$project_config.(\w+)")
36
+ ORG_CONFIG_RE = re.compile(r"\$org_config.(\w+)")
36
37
  CAPTURE_TASK_OUTPUT = os.environ.get("CAPTURE_TASK_OUTPUT")
37
38
 
38
39
 
@@ -135,12 +136,22 @@ class BaseTask:
135
136
  self.options.update(kwargs)
136
137
 
137
138
  # Handle dynamic lookup of project_config values via $project_config.attr
139
+ # and org_config values via $org_config.attr
138
140
  def process_options(option):
139
141
  if isinstance(option, str):
140
- return PROJECT_CONFIG_RE.sub(
142
+ # Replace $project_config.attr patterns
143
+ option = PROJECT_CONFIG_RE.sub(
141
144
  lambda match: str(self.project_config.lookup(match.group(1), None)),
142
145
  option,
143
146
  )
147
+ # Replace $org_config.attr patterns if org_config is available
148
+ if self.org_config is not None:
149
+ org_config = self.org_config # Capture for type narrowing
150
+ option = ORG_CONFIG_RE.sub(
151
+ lambda match: str(org_config.lookup(match.group(1), None)),
152
+ option,
153
+ )
154
+ return option
144
155
  elif isinstance(option, dict):
145
156
  processed_dict = {}
146
157
  for key, value in option.items():