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.
Files changed (123) 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 +17 -0
  5. cumulusci/cli/tests/test_error.py +3 -1
  6. cumulusci/cli/tests/test_flow.py +279 -2
  7. cumulusci/cli/tests/test_service.py +15 -12
  8. cumulusci/cli/tests/test_task.py +88 -2
  9. cumulusci/cli/tests/utils.py +1 -4
  10. cumulusci/core/config/base_task_flow_config.py +26 -1
  11. cumulusci/core/config/project_config.py +2 -20
  12. cumulusci/core/config/tests/test_config_expensive.py +9 -3
  13. cumulusci/core/config/universal_config.py +3 -4
  14. cumulusci/core/dependencies/base.py +1 -1
  15. cumulusci/core/dependencies/dependencies.py +1 -1
  16. cumulusci/core/dependencies/github.py +1 -2
  17. cumulusci/core/dependencies/resolvers.py +1 -1
  18. cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
  19. cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
  20. cumulusci/core/flowrunner.py +90 -6
  21. cumulusci/core/github.py +1 -1
  22. cumulusci/core/sfdx.py +3 -1
  23. cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
  24. cumulusci/core/source_transforms/transforms.py +1 -1
  25. cumulusci/core/tasks.py +13 -2
  26. cumulusci/core/tests/test_flowrunner.py +100 -0
  27. cumulusci/core/tests/test_tasks.py +65 -0
  28. cumulusci/core/utils.py +3 -1
  29. cumulusci/core/versions.py +1 -1
  30. cumulusci/cumulusci.yml +55 -0
  31. cumulusci/oauth/client.py +1 -1
  32. cumulusci/plugins/plugin_base.py +5 -3
  33. cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
  34. cumulusci/salesforce_api/rest_deploy.py +1 -1
  35. cumulusci/schema/cumulusci.jsonschema.json +64 -0
  36. cumulusci/tasks/apex/anon.py +1 -1
  37. cumulusci/tasks/apex/testrunner.py +416 -142
  38. cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
  39. cumulusci/tasks/bulkdata/extract.py +0 -1
  40. cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
  41. cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
  42. cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
  43. cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
  44. cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
  45. cumulusci/tasks/bulkdata/select_utils.py +1 -1
  46. cumulusci/tasks/bulkdata/snowfakery.py +100 -25
  47. cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
  48. cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
  49. cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
  50. cumulusci/tasks/bulkdata/tests/test_select_utils.py +26 -0
  51. cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
  52. cumulusci/tasks/create_package_version.py +190 -16
  53. cumulusci/tasks/datadictionary.py +1 -1
  54. cumulusci/tasks/metadata_etl/base.py +7 -3
  55. cumulusci/tasks/metadata_etl/layouts.py +1 -1
  56. cumulusci/tasks/metadata_etl/permissions.py +1 -1
  57. cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
  58. cumulusci/tasks/push/README.md +15 -17
  59. cumulusci/tasks/release_notes/README.md +13 -13
  60. cumulusci/tasks/release_notes/generator.py +13 -8
  61. cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
  62. cumulusci/tasks/salesforce/Deploy.py +53 -2
  63. cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
  64. cumulusci/tasks/salesforce/__init__.py +1 -0
  65. cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
  66. cumulusci/tasks/salesforce/composite.py +1 -1
  67. cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
  68. cumulusci/tasks/salesforce/enable_prediction.py +5 -1
  69. cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
  70. cumulusci/tasks/salesforce/profiles.py +13 -9
  71. cumulusci/tasks/salesforce/sourcetracking.py +1 -1
  72. cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
  73. cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
  74. cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
  75. cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
  76. cumulusci/tasks/salesforce/tests/test_profiles.py +43 -3
  77. cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
  78. cumulusci/tasks/salesforce/tests/test_update_external_credential.py +912 -0
  79. cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
  80. cumulusci/tasks/salesforce/update_dependencies.py +2 -2
  81. cumulusci/tasks/salesforce/update_external_credential.py +562 -0
  82. cumulusci/tasks/salesforce/update_named_credential.py +441 -0
  83. cumulusci/tasks/salesforce/update_profile.py +17 -13
  84. cumulusci/tasks/salesforce/users/permsets.py +62 -5
  85. cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
  86. cumulusci/tasks/sfdmu/__init__.py +0 -0
  87. cumulusci/tasks/sfdmu/sfdmu.py +363 -0
  88. cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
  89. cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
  90. cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
  91. cumulusci/tasks/tests/test_create_package_version.py +716 -1
  92. cumulusci/tasks/tests/test_util.py +42 -0
  93. cumulusci/tasks/util.py +37 -1
  94. cumulusci/tasks/utility/copyContents.py +402 -0
  95. cumulusci/tasks/utility/credentialManager.py +256 -0
  96. cumulusci/tasks/utility/directoryRecreator.py +30 -0
  97. cumulusci/tasks/utility/env_management.py +1 -1
  98. cumulusci/tasks/utility/secretsToEnv.py +135 -0
  99. cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
  100. cumulusci/tasks/utility/tests/test_credentialManager.py +564 -0
  101. cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
  102. cumulusci/tasks/utility/tests/test_secretsToEnv.py +1091 -0
  103. cumulusci/tests/test_integration_infrastructure.py +3 -1
  104. cumulusci/tests/test_utils.py +70 -6
  105. cumulusci/utils/__init__.py +54 -9
  106. cumulusci/utils/classutils.py +5 -2
  107. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
  108. cumulusci/utils/options.py +23 -1
  109. cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
  110. cumulusci/utils/yaml/cumulusci_yml.py +7 -3
  111. cumulusci/utils/yaml/model_parser.py +2 -2
  112. cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
  113. cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
  114. cumulusci/vcs/base.py +23 -15
  115. cumulusci/vcs/bootstrap.py +5 -4
  116. cumulusci/vcs/utils/list_modified_files.py +189 -0
  117. cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
  118. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/METADATA +12 -10
  119. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/RECORD +123 -98
  120. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/WHEEL +0 -0
  121. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/entry_points.txt +0 -0
  122. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/AUTHORS.rst +0 -0
  123. {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
- # 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
 
@@ -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
@@ -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():
@@ -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(options["managed"])
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)
@@ -1,7 +1,7 @@
1
1
  import re
2
2
  from typing import Optional, Union
3
3
 
4
- from pydantic import BaseModel
4
+ from pydantic.v1 import BaseModel
5
5
 
6
6
  from cumulusci.core.enums import StrEnum
7
7