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.
- cumulusci/__about__.py +1 -1
- cumulusci/cli/logger.py +2 -2
- cumulusci/cli/service.py +20 -0
- cumulusci/cli/task.py +19 -3
- cumulusci/cli/tests/test_error.py +3 -1
- cumulusci/cli/tests/test_flow.py +279 -2
- cumulusci/cli/tests/test_org.py +5 -0
- cumulusci/cli/tests/test_service.py +15 -12
- cumulusci/cli/tests/test_task.py +122 -2
- cumulusci/cli/tests/utils.py +1 -4
- cumulusci/core/config/__init__.py +1 -0
- cumulusci/core/config/base_task_flow_config.py +26 -1
- cumulusci/core/config/org_config.py +2 -1
- cumulusci/core/config/project_config.py +14 -20
- cumulusci/core/config/scratch_org_config.py +12 -0
- cumulusci/core/config/tests/test_config.py +1 -0
- cumulusci/core/config/tests/test_config_expensive.py +9 -3
- cumulusci/core/config/universal_config.py +3 -4
- cumulusci/core/dependencies/base.py +5 -1
- cumulusci/core/dependencies/dependencies.py +1 -1
- cumulusci/core/dependencies/github.py +1 -2
- cumulusci/core/dependencies/resolvers.py +1 -1
- cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
- cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
- cumulusci/core/flowrunner.py +90 -6
- cumulusci/core/github.py +1 -1
- cumulusci/core/sfdx.py +3 -1
- cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
- cumulusci/core/source_transforms/transforms.py +1 -1
- cumulusci/core/tasks.py +13 -2
- cumulusci/core/tests/test_flowrunner.py +100 -0
- cumulusci/core/tests/test_tasks.py +65 -0
- cumulusci/core/utils.py +3 -1
- cumulusci/core/versions.py +1 -1
- cumulusci/cumulusci.yml +73 -1
- cumulusci/oauth/client.py +1 -1
- cumulusci/plugins/plugin_base.py +5 -3
- cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
- cumulusci/salesforce_api/rest_deploy.py +1 -1
- cumulusci/schema/cumulusci.jsonschema.json +69 -0
- cumulusci/tasks/apex/anon.py +1 -1
- cumulusci/tasks/apex/testrunner.py +421 -144
- cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
- cumulusci/tasks/bulkdata/extract.py +0 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
- cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
- cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
- cumulusci/tasks/bulkdata/select_utils.py +1 -1
- cumulusci/tasks/bulkdata/snowfakery.py +100 -25
- cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
- cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
- cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
- cumulusci/tasks/bulkdata/tests/test_select_utils.py +46 -0
- cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
- cumulusci/tasks/create_package_version.py +190 -16
- cumulusci/tasks/datadictionary.py +1 -1
- cumulusci/tasks/metadata_etl/__init__.py +2 -0
- cumulusci/tasks/metadata_etl/applications.py +256 -0
- cumulusci/tasks/metadata_etl/base.py +7 -3
- cumulusci/tasks/metadata_etl/layouts.py +1 -1
- cumulusci/tasks/metadata_etl/permissions.py +1 -1
- cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
- cumulusci/tasks/metadata_etl/tests/test_applications.py +710 -0
- cumulusci/tasks/push/README.md +15 -17
- cumulusci/tasks/release_notes/README.md +13 -13
- cumulusci/tasks/release_notes/generator.py +13 -8
- cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
- cumulusci/tasks/salesforce/Deploy.py +53 -2
- cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
- cumulusci/tasks/salesforce/__init__.py +1 -0
- cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
- cumulusci/tasks/salesforce/composite.py +1 -1
- cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
- cumulusci/tasks/salesforce/enable_prediction.py +5 -1
- cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
- cumulusci/tasks/salesforce/insert_record.py +18 -19
- cumulusci/tasks/salesforce/sourcetracking.py +1 -1
- cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
- cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
- cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
- cumulusci/tasks/salesforce/tests/test_enable_prediction.py +4 -2
- cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
- cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
- cumulusci/tasks/salesforce/tests/test_update_external_auth_identity_provider.py +927 -0
- cumulusci/tasks/salesforce/tests/test_update_external_credential.py +1427 -0
- cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
- cumulusci/tasks/salesforce/tests/test_update_record.py +512 -0
- cumulusci/tasks/salesforce/update_dependencies.py +2 -2
- cumulusci/tasks/salesforce/update_external_auth_identity_provider.py +551 -0
- cumulusci/tasks/salesforce/update_external_credential.py +647 -0
- cumulusci/tasks/salesforce/update_named_credential.py +441 -0
- cumulusci/tasks/salesforce/update_profile.py +17 -13
- cumulusci/tasks/salesforce/update_record.py +217 -0
- cumulusci/tasks/salesforce/users/permsets.py +62 -5
- cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
- cumulusci/tasks/sfdmu/__init__.py +0 -0
- cumulusci/tasks/sfdmu/sfdmu.py +376 -0
- cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
- cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
- cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
- cumulusci/tasks/tests/test_create_package_version.py +716 -1
- cumulusci/tasks/tests/test_util.py +42 -0
- cumulusci/tasks/util.py +37 -1
- cumulusci/tasks/utility/copyContents.py +402 -0
- cumulusci/tasks/utility/credentialManager.py +302 -0
- cumulusci/tasks/utility/directoryRecreator.py +30 -0
- cumulusci/tasks/utility/env_management.py +1 -1
- cumulusci/tasks/utility/secretsToEnv.py +135 -0
- cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
- cumulusci/tasks/utility/tests/test_credentialManager.py +1150 -0
- cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
- cumulusci/tasks/utility/tests/test_secretsToEnv.py +1118 -0
- cumulusci/tests/test_integration_infrastructure.py +3 -1
- cumulusci/tests/test_utils.py +70 -6
- cumulusci/utils/__init__.py +54 -9
- cumulusci/utils/classutils.py +5 -2
- cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
- cumulusci/utils/options.py +23 -1
- cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
- cumulusci/utils/yaml/cumulusci_yml.py +8 -3
- cumulusci/utils/yaml/model_parser.py +2 -2
- cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
- cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
- cumulusci/vcs/base.py +23 -15
- cumulusci/vcs/bootstrap.py +5 -4
- cumulusci/vcs/utils/list_modified_files.py +189 -0
- cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/METADATA +11 -10
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/RECORD +135 -104
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/WHEEL +1 -1
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/LICENSE +0 -0
cumulusci/cli/tests/utils.py
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
@@ -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
|
-
|
|
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}"]
|
|
@@ -54,7 +54,9 @@ class TestUniversalConfig:
|
|
|
54
54
|
with open(filename, "w") as f:
|
|
55
55
|
f.write(content)
|
|
56
56
|
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
cumulusci/core/flowrunner.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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():
|