cumulusci-plus 5.0.19__py3-none-any.whl → 5.0.35__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 +17 -0
- cumulusci/cli/tests/test_error.py +3 -1
- cumulusci/cli/tests/test_flow.py +279 -2
- cumulusci/cli/tests/test_service.py +15 -12
- cumulusci/cli/tests/test_task.py +88 -2
- cumulusci/cli/tests/utils.py +1 -4
- cumulusci/core/config/base_task_flow_config.py +26 -1
- cumulusci/core/config/project_config.py +2 -20
- cumulusci/core/config/tests/test_config_expensive.py +9 -3
- cumulusci/core/config/universal_config.py +3 -4
- cumulusci/core/dependencies/base.py +1 -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 +55 -0
- 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 +64 -0
- cumulusci/tasks/apex/anon.py +1 -1
- cumulusci/tasks/apex/testrunner.py +416 -142
- 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 +26 -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/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/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/profiles.py +13 -9
- 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_getPackageVersion.py +651 -0
- cumulusci/tasks/salesforce/tests/test_profiles.py +43 -3
- cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
- cumulusci/tasks/salesforce/tests/test_update_external_credential.py +912 -0
- cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
- cumulusci/tasks/salesforce/update_dependencies.py +2 -2
- cumulusci/tasks/salesforce/update_external_credential.py +562 -0
- cumulusci/tasks/salesforce/update_named_credential.py +441 -0
- cumulusci/tasks/salesforce/update_profile.py +17 -13
- 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 +363 -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 +256 -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 +564 -0
- cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
- cumulusci/tasks/utility/tests/test_secretsToEnv.py +1091 -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 +7 -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.19.dist-info → cumulusci_plus-5.0.35.dist-info}/METADATA +12 -10
- {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/RECORD +123 -98
- {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/WHEEL +0 -0
- {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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():
|
|
@@ -542,6 +542,106 @@ class TestSimpleTestFlowCoordinator(AbstractFlowCoordinatorTest):
|
|
|
542
542
|
flow.run(self.org_config)
|
|
543
543
|
assert len(flow.results) == 0
|
|
544
544
|
|
|
545
|
+
def test_run__when_condition_with_env_var_true(self):
|
|
546
|
+
"""A flow step runs when env var condition evaluates to True"""
|
|
547
|
+
import os
|
|
548
|
+
|
|
549
|
+
with mock.patch.dict(os.environ, {"RUN_TASK": "true"}):
|
|
550
|
+
flow_config = FlowConfig(
|
|
551
|
+
{
|
|
552
|
+
"steps": {
|
|
553
|
+
1: {
|
|
554
|
+
"task": "pass_name",
|
|
555
|
+
"when": "env.get('RUN_TASK') == 'true'",
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
)
|
|
560
|
+
flow = FlowCoordinator(self.project_config, flow_config)
|
|
561
|
+
flow.run(self.org_config)
|
|
562
|
+
assert len(flow.results) == 1
|
|
563
|
+
|
|
564
|
+
def test_run__when_condition_with_env_var_false(self):
|
|
565
|
+
"""A flow step is skipped when env var condition evaluates to False"""
|
|
566
|
+
import os
|
|
567
|
+
|
|
568
|
+
with mock.patch.dict(os.environ, {"RUN_TASK": "false"}):
|
|
569
|
+
flow_config = FlowConfig(
|
|
570
|
+
{
|
|
571
|
+
"steps": {
|
|
572
|
+
1: {
|
|
573
|
+
"task": "pass_name",
|
|
574
|
+
"when": "env.get('RUN_TASK') == 'true'",
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
flow = FlowCoordinator(self.project_config, flow_config)
|
|
580
|
+
flow.run(self.org_config)
|
|
581
|
+
assert len(flow.results) == 0
|
|
582
|
+
|
|
583
|
+
def test_run__when_condition_with_env_var_default(self):
|
|
584
|
+
"""A flow step uses default value when env var is not set"""
|
|
585
|
+
import os
|
|
586
|
+
|
|
587
|
+
# Make sure the env var is not set
|
|
588
|
+
if "UNDEFINED_VAR" in os.environ:
|
|
589
|
+
del os.environ["UNDEFINED_VAR"]
|
|
590
|
+
flow_config = FlowConfig(
|
|
591
|
+
{
|
|
592
|
+
"steps": {
|
|
593
|
+
1: {
|
|
594
|
+
"task": "pass_name",
|
|
595
|
+
"when": "env.get('UNDEFINED_VAR', 'default') == 'default'",
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
)
|
|
600
|
+
flow = FlowCoordinator(self.project_config, flow_config)
|
|
601
|
+
flow.run(self.org_config)
|
|
602
|
+
assert len(flow.results) == 1
|
|
603
|
+
|
|
604
|
+
def test_run__when_condition_with_env_var_and_org_config(self):
|
|
605
|
+
"""A flow step can combine env vars with org_config in when condition"""
|
|
606
|
+
import os
|
|
607
|
+
|
|
608
|
+
with mock.patch.dict(os.environ, {"DEPLOY_TO_SCRATCH": "true"}):
|
|
609
|
+
flow_config = FlowConfig(
|
|
610
|
+
{
|
|
611
|
+
"steps": {
|
|
612
|
+
1: {
|
|
613
|
+
"task": "pass_name",
|
|
614
|
+
"when": "org_config.scratch and env.get('DEPLOY_TO_SCRATCH') == 'true'",
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
)
|
|
619
|
+
flow = FlowCoordinator(self.project_config, flow_config)
|
|
620
|
+
# Set org_config.scratch to True
|
|
621
|
+
self.org_config.config["scratch"] = True
|
|
622
|
+
flow.run(self.org_config)
|
|
623
|
+
assert len(flow.results) == 1
|
|
624
|
+
|
|
625
|
+
def test_run__when_condition_with_missing_env_var_is_none(self):
|
|
626
|
+
"""A flow step can check if env var exists using 'is not none'"""
|
|
627
|
+
import os
|
|
628
|
+
|
|
629
|
+
# Test when var is set
|
|
630
|
+
with mock.patch.dict(os.environ, {"MY_VAR": "value"}):
|
|
631
|
+
flow_config = FlowConfig(
|
|
632
|
+
{
|
|
633
|
+
"steps": {
|
|
634
|
+
1: {
|
|
635
|
+
"task": "pass_name",
|
|
636
|
+
"when": "env.get('MY_VAR') is not none",
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
)
|
|
641
|
+
flow = FlowCoordinator(self.project_config, flow_config)
|
|
642
|
+
flow.run(self.org_config)
|
|
643
|
+
assert len(flow.results) == 1
|
|
644
|
+
|
|
545
645
|
def test_run__task_raises_exception_fail(self):
|
|
546
646
|
"""A flow aborts when a task raises an exception"""
|
|
547
647
|
|
|
@@ -147,6 +147,71 @@ class TestBaseTaskCallable:
|
|
|
147
147
|
task = BaseTask(self.project_config, self.task_config, self.org_config)
|
|
148
148
|
assert task.options["test_option"] == "before baz after"
|
|
149
149
|
|
|
150
|
+
def test_init_options__org_config_substitution(self):
|
|
151
|
+
self.task_config.config["options"] = {"test_option": "$org_config.username"}
|
|
152
|
+
task = BaseTask(self.project_config, self.task_config, self.org_config)
|
|
153
|
+
assert task.options["test_option"] == USERNAME
|
|
154
|
+
|
|
155
|
+
def test_init_options__org_config_substitution_org_id(self):
|
|
156
|
+
self.task_config.config["options"] = {"test_option": "$org_config.org_id"}
|
|
157
|
+
task = BaseTask(self.project_config, self.task_config, self.org_config)
|
|
158
|
+
assert task.options["test_option"] == ORG_ID
|
|
159
|
+
|
|
160
|
+
def test_init_options__org_config_substitution_nested(self):
|
|
161
|
+
self.task_config.config["options"] = {
|
|
162
|
+
"test_option": "$org_config.username",
|
|
163
|
+
"nested": [
|
|
164
|
+
{"user": "$org_config.username"},
|
|
165
|
+
{"org": "$org_config.org_id"},
|
|
166
|
+
],
|
|
167
|
+
}
|
|
168
|
+
task = BaseTask(self.project_config, self.task_config, self.org_config)
|
|
169
|
+
assert task.options["test_option"] == USERNAME
|
|
170
|
+
assert task.options["nested"][0]["user"] == USERNAME
|
|
171
|
+
assert task.options["nested"][1]["org"] == ORG_ID
|
|
172
|
+
|
|
173
|
+
def test_init_options__org_config_substitution__substring(self):
|
|
174
|
+
self.task_config.config["options"] = {
|
|
175
|
+
"test_option": "User: $org_config.username, Org: $org_config.org_id"
|
|
176
|
+
}
|
|
177
|
+
task = BaseTask(self.project_config, self.task_config, self.org_config)
|
|
178
|
+
assert task.options["test_option"] == f"User: {USERNAME}, Org: {ORG_ID}"
|
|
179
|
+
|
|
180
|
+
def test_init_options__org_config_substitution__no_org(self):
|
|
181
|
+
"""Test that $org_config substitution is skipped when org_config is None"""
|
|
182
|
+
self.task_config.config["options"] = {"test_option": "$org_config.username"}
|
|
183
|
+
task = BaseTask(self.project_config, self.task_config, org_config=None)
|
|
184
|
+
# Should remain unchanged when org_config is None
|
|
185
|
+
assert task.options["test_option"] == "$org_config.username"
|
|
186
|
+
|
|
187
|
+
def test_init_options__mixed_substitution(self):
|
|
188
|
+
"""Test that both $project_config and $org_config work together"""
|
|
189
|
+
self.project_config.config["foo"] = {"bar": "baz"}
|
|
190
|
+
self.task_config.config["options"] = {
|
|
191
|
+
"test_option": "Project: $project_config.foo__bar, User: $org_config.username"
|
|
192
|
+
}
|
|
193
|
+
task = BaseTask(self.project_config, self.task_config, self.org_config)
|
|
194
|
+
assert task.options["test_option"] == f"Project: baz, User: {USERNAME}"
|
|
195
|
+
|
|
196
|
+
def test_init_options__mixed_substitution_nested(self):
|
|
197
|
+
"""Test mixed substitution in nested structures"""
|
|
198
|
+
self.project_config.config["project"] = {"name": "TestProject"}
|
|
199
|
+
self.task_config.config["options"] = {
|
|
200
|
+
"config": {
|
|
201
|
+
"project_name": "$project_config.project__name",
|
|
202
|
+
"username": "$org_config.username",
|
|
203
|
+
"items": [
|
|
204
|
+
"$project_config.project__name",
|
|
205
|
+
"$org_config.org_id",
|
|
206
|
+
],
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
task = BaseTask(self.project_config, self.task_config, self.org_config)
|
|
210
|
+
assert task.options["config"]["project_name"] == "TestProject"
|
|
211
|
+
assert task.options["config"]["username"] == USERNAME
|
|
212
|
+
assert task.options["config"]["items"][0] == "TestProject"
|
|
213
|
+
assert task.options["config"]["items"][1] == ORG_ID
|
|
214
|
+
|
|
150
215
|
def test_validates_missing_options(self):
|
|
151
216
|
class Task(BaseTask):
|
|
152
217
|
task_options = {"test_option": {"required": True}}
|
cumulusci/core/utils.py
CHANGED
|
@@ -379,7 +379,9 @@ def determine_managed_mode(options, project_config, org_config):
|
|
|
379
379
|
Note: The changes allows multiple package development under same namespace.
|
|
380
380
|
"""
|
|
381
381
|
if "managed" in options:
|
|
382
|
-
return process_bool_arg(
|
|
382
|
+
return process_bool_arg(
|
|
383
|
+
options["managed"] if options["managed"] is not None else False
|
|
384
|
+
)
|
|
383
385
|
|
|
384
386
|
# Get package and namespace information
|
|
385
387
|
package_name = getattr(project_config, "project__package__name", None)
|