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
|
@@ -35,6 +35,7 @@ def capture_orgid_using_task(create_task: callable, tmp_path: str) -> str:
|
|
|
35
35
|
|
|
36
36
|
class TestIntegrationInfrastructure:
|
|
37
37
|
"Test our two plugins for doing integration testing"
|
|
38
|
+
|
|
38
39
|
remembered_cli_specified_org_id = None
|
|
39
40
|
|
|
40
41
|
@pytest.mark.vcr()
|
|
@@ -42,6 +43,7 @@ class TestIntegrationInfrastructure:
|
|
|
42
43
|
self, create_task, run_code_without_recording
|
|
43
44
|
):
|
|
44
45
|
"Delete a VCR Cassette to ensure next test creates it."
|
|
46
|
+
|
|
45
47
|
# only delete the cassette if we can replace it
|
|
46
48
|
def delete_cassette():
|
|
47
49
|
if first_cassette.exists():
|
|
@@ -81,7 +83,7 @@ class TestIntegrationInfrastructure:
|
|
|
81
83
|
|
|
82
84
|
run_code_without_recording(setup)
|
|
83
85
|
|
|
84
|
-
def test_file_was_not_created(self
|
|
86
|
+
def test_file_was_not_created(self):
|
|
85
87
|
filename = (
|
|
86
88
|
Path(__file__).parent
|
|
87
89
|
/ "cassettes/TestIntegrationInfrastructure.test_run_code_without_recording.yaml"
|
cumulusci/tests/test_utils.py
CHANGED
|
@@ -453,31 +453,95 @@ Options\n------------------------------------------\n\n
|
|
|
453
453
|
def test_inject_namespace__managed(self):
|
|
454
454
|
logger = mock.Mock()
|
|
455
455
|
name = "___NAMESPACE___test"
|
|
456
|
-
content = "%%%NAMESPACE%%%|%%%NAMESPACE_DOT%%%|%%%NAMESPACED_ORG%%%|%%%NAMESPACE_OR_C%%%|%%%NAMESPACED_ORG_OR_C%%%"
|
|
456
|
+
content = "%%%NAMESPACE%%%|%%%NAMESPACE_DOT%%%|%%%NAMESPACED_ORG%%%|%%%NAMESPACE_OR_C%%%|%%%NAMESPACED_ORG_OR_C%%%|%%%NAMESPACE_COLON%%%|%%%NAMESPACED_ORG_COLON%%%"
|
|
457
457
|
|
|
458
458
|
name, content = utils.inject_namespace(
|
|
459
459
|
name, content, namespace="ns", managed=True, logger=logger
|
|
460
460
|
)
|
|
461
461
|
assert name == "ns__test"
|
|
462
|
-
assert content == "ns__|ns.||ns|c"
|
|
462
|
+
assert content == "ns__|ns.||ns|c|ns:|"
|
|
463
463
|
|
|
464
464
|
def test_inject_namespace__unmanaged(self):
|
|
465
465
|
name = "___NAMESPACE___test"
|
|
466
|
-
content = "%%%NAMESPACE%%%|%%%NAMESPACE_DOT%%%|%%%NAMESPACED_ORG%%%|%%%NAMESPACE_OR_C%%%|%%%NAMESPACED_ORG_OR_C%%%"
|
|
466
|
+
content = "%%%NAMESPACE%%%|%%%NAMESPACE_DOT%%%|%%%NAMESPACED_ORG%%%|%%%NAMESPACE_OR_C%%%|%%%NAMESPACED_ORG_OR_C%%%|%%%NAMESPACE_COLON%%%|%%%NAMESPACED_ORG_COLON%%%"
|
|
467
467
|
|
|
468
468
|
name, content = utils.inject_namespace(name, content, namespace="ns")
|
|
469
469
|
assert name == "test"
|
|
470
|
-
assert content == "|||c|c"
|
|
470
|
+
assert content == "|||c|c||"
|
|
471
471
|
|
|
472
472
|
def test_inject_namespace__namespaced_org(self):
|
|
473
473
|
name = "___NAMESPACE___test"
|
|
474
|
-
content = "%%%NAMESPACE%%%|%%%NAMESPACE_DOT%%%|%%%NAMESPACED_ORG%%%|%%%NAMESPACE_OR_C%%%|%%%NAMESPACED_ORG_OR_C%%%"
|
|
474
|
+
content = "%%%NAMESPACE%%%|%%%NAMESPACE_DOT%%%|%%%NAMESPACED_ORG%%%|%%%NAMESPACE_OR_C%%%|%%%NAMESPACED_ORG_OR_C%%%|%%%NAMESPACE_COLON%%%|%%%NAMESPACED_ORG_COLON%%%"
|
|
475
475
|
|
|
476
476
|
name, content = utils.inject_namespace(
|
|
477
477
|
name, content, namespace="ns", managed=True, namespaced_org=True
|
|
478
478
|
)
|
|
479
479
|
assert name == "ns__test"
|
|
480
|
-
assert content == "ns__|ns.|ns__|ns|ns"
|
|
480
|
+
assert content == "ns__|ns.|ns__|ns|ns|ns:|ns:"
|
|
481
|
+
|
|
482
|
+
def test_inject_namespace__namespace_colon_managed(self):
|
|
483
|
+
"""Test %%%NAMESPACE_COLON%%% token with managed=True"""
|
|
484
|
+
logger = mock.Mock()
|
|
485
|
+
name = "test"
|
|
486
|
+
content = "%%%NAMESPACE_COLON%%%component"
|
|
487
|
+
|
|
488
|
+
name, content = utils.inject_namespace(
|
|
489
|
+
name, content, namespace="ns", managed=True, logger=logger
|
|
490
|
+
)
|
|
491
|
+
assert content == "ns:component"
|
|
492
|
+
logger.info.assert_called()
|
|
493
|
+
|
|
494
|
+
def test_inject_namespace__namespace_colon_unmanaged(self):
|
|
495
|
+
"""Test %%%NAMESPACE_COLON%%% token with managed=False"""
|
|
496
|
+
name = "test"
|
|
497
|
+
content = "%%%NAMESPACE_COLON%%%component"
|
|
498
|
+
|
|
499
|
+
name, content = utils.inject_namespace(
|
|
500
|
+
name, content, namespace="ns", managed=False
|
|
501
|
+
)
|
|
502
|
+
assert content == "component"
|
|
503
|
+
|
|
504
|
+
def test_inject_namespace__namespace_colon_no_namespace(self):
|
|
505
|
+
"""Test %%%NAMESPACE_COLON%%% token with no namespace"""
|
|
506
|
+
name = "test"
|
|
507
|
+
content = "%%%NAMESPACE_COLON%%%component"
|
|
508
|
+
|
|
509
|
+
name, content = utils.inject_namespace(
|
|
510
|
+
name, content, namespace=None, managed=True
|
|
511
|
+
)
|
|
512
|
+
assert content == "component"
|
|
513
|
+
|
|
514
|
+
def test_inject_namespace__namespaced_org_colon(self):
|
|
515
|
+
"""Test %%%NAMESPACED_ORG_COLON%%% token with namespaced_org=True"""
|
|
516
|
+
logger = mock.Mock()
|
|
517
|
+
name = "test"
|
|
518
|
+
content = "%%%NAMESPACED_ORG_COLON%%%component"
|
|
519
|
+
|
|
520
|
+
name, content = utils.inject_namespace(
|
|
521
|
+
name, content, namespace="ns", namespaced_org=True, logger=logger
|
|
522
|
+
)
|
|
523
|
+
assert content == "ns:component"
|
|
524
|
+
logger.info.assert_called()
|
|
525
|
+
|
|
526
|
+
def test_inject_namespace__namespaced_org_colon_false(self):
|
|
527
|
+
"""Test %%%NAMESPACED_ORG_COLON%%% token with namespaced_org=False"""
|
|
528
|
+
name = "test"
|
|
529
|
+
content = "%%%NAMESPACED_ORG_COLON%%%component"
|
|
530
|
+
|
|
531
|
+
name, content = utils.inject_namespace(
|
|
532
|
+
name, content, namespace="ns", namespaced_org=False
|
|
533
|
+
)
|
|
534
|
+
assert content == "component"
|
|
535
|
+
|
|
536
|
+
def test_inject_namespace__namespaced_org_colon_no_namespace(self):
|
|
537
|
+
"""Test %%%NAMESPACED_ORG_COLON%%% token with no namespace"""
|
|
538
|
+
name = "test"
|
|
539
|
+
content = "%%%NAMESPACED_ORG_COLON%%%component"
|
|
540
|
+
|
|
541
|
+
name, content = utils.inject_namespace(
|
|
542
|
+
name, content, namespace=None, namespaced_org=True
|
|
543
|
+
)
|
|
544
|
+
assert content == "component"
|
|
481
545
|
|
|
482
546
|
def test_inject_namespace__filename_token_in_package_xml(self):
|
|
483
547
|
name, content = utils.inject_namespace(
|
cumulusci/utils/__init__.py
CHANGED
|
@@ -42,7 +42,7 @@ BREW_DEPRECATION_MSG = (
|
|
|
42
42
|
"brew uninstall cumulusci-plus\nbrew install pipx\npipx ensurepath\npipx install cumulusci-plus"
|
|
43
43
|
)
|
|
44
44
|
PIP_UPDATE_CMD = "pip install --upgrade cumulusci-plus"
|
|
45
|
-
PIPX_UPDATE_CMD = "pipx
|
|
45
|
+
PIPX_UPDATE_CMD = "pipx install cumulusci-plus-azure-devops --include-deps --force"
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def parse_api_datetime(value):
|
|
@@ -245,6 +245,10 @@ def inject_namespace(
|
|
|
245
245
|
):
|
|
246
246
|
"""Replaces %%%NAMESPACE%%% in file content and ___NAMESPACE___ in file name
|
|
247
247
|
with either '' if no namespace is provided or 'namespace__' if provided.
|
|
248
|
+
|
|
249
|
+
Also handles:
|
|
250
|
+
- %%%MANAGED_OR_NAMESPACED_ORG%%% and ___MANAGED_OR_NAMESPACED_ORG___ tokens
|
|
251
|
+
which are replaced with 'namespace__' if managed=True OR namespaced_org=True
|
|
248
252
|
"""
|
|
249
253
|
|
|
250
254
|
# Handle namespace and filename tokens
|
|
@@ -264,7 +268,15 @@ def inject_namespace(
|
|
|
264
268
|
# Handle tokens %%%NAMESPACED_ORG%%% and ___NAMESPACED_ORG___
|
|
265
269
|
namespaced_org_token = "%%%NAMESPACED_ORG%%%"
|
|
266
270
|
namespaced_org_file_token = "___NAMESPACED_ORG___"
|
|
267
|
-
namespaced_org = namespace + "__" if namespaced_org else ""
|
|
271
|
+
namespaced_org = (namespace + "__") if namespaced_org and namespace else ""
|
|
272
|
+
|
|
273
|
+
# Handle tokens %%%NAMESPACED_ORG_COLON%%%
|
|
274
|
+
namespaced_org_colon_token = "%%%NAMESPACED_ORG_COLON%%%"
|
|
275
|
+
namespaced_org_colon = (namespace + ":") if namespaced_org and namespace else ""
|
|
276
|
+
|
|
277
|
+
# Handle tokens %%%NAMESPACE_COLON%%%
|
|
278
|
+
namespace_colon_token = "%%%NAMESPACE_COLON%%%"
|
|
279
|
+
namespace_colon = (namespace + ":") if managed and namespace else ""
|
|
268
280
|
|
|
269
281
|
# Handle token %%%NAMESPACE_OR_C%%% for lightning components
|
|
270
282
|
namespace_or_c_token = "%%%NAMESPACE_OR_C%%%"
|
|
@@ -274,6 +286,13 @@ def inject_namespace(
|
|
|
274
286
|
namespaced_org_or_c_token = "%%%NAMESPACED_ORG_OR_C%%%"
|
|
275
287
|
namespaced_org_or_c = namespace if namespaced_org else "c"
|
|
276
288
|
|
|
289
|
+
# Handle new tokens %%%MANAGED_OR_NAMESPACED_ORG%%% and ___MANAGED_OR_NAMESPACED_ORG___
|
|
290
|
+
managed_or_namespaced_org_token = "%%%MANAGED_OR_NAMESPACED_ORG%%%"
|
|
291
|
+
managed_or_namespaced_org_file_token = "___MANAGED_OR_NAMESPACED_ORG___"
|
|
292
|
+
managed_or_namespaced_org = (
|
|
293
|
+
(namespace + "__") if ((managed) or (namespaced_org)) and namespace else ""
|
|
294
|
+
)
|
|
295
|
+
|
|
277
296
|
orig_name = name
|
|
278
297
|
prev_content = content
|
|
279
298
|
content = content.replace(namespace_token, namespace_prefix)
|
|
@@ -317,6 +336,20 @@ def inject_namespace(
|
|
|
317
336
|
f' {name}: Replaced {namespaced_org_token} with "{namespaced_org}"'
|
|
318
337
|
)
|
|
319
338
|
|
|
339
|
+
prev_content = content
|
|
340
|
+
content = content.replace(namespaced_org_colon_token, namespaced_org_colon)
|
|
341
|
+
if logger and content != prev_content:
|
|
342
|
+
logger.info(
|
|
343
|
+
f' {name}: Replaced {namespaced_org_colon_token} with "{namespaced_org_colon}"'
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
prev_content = content
|
|
347
|
+
content = content.replace(namespace_colon_token, namespace_colon)
|
|
348
|
+
if logger and content != prev_content:
|
|
349
|
+
logger.info(
|
|
350
|
+
f' {name}: Replaced {namespace_colon_token} with "{namespace_colon}"'
|
|
351
|
+
)
|
|
352
|
+
|
|
320
353
|
prev_content = content
|
|
321
354
|
content = content.replace(namespaced_org_or_c_token, namespaced_org_or_c)
|
|
322
355
|
if logger and content != prev_content:
|
|
@@ -324,9 +357,20 @@ def inject_namespace(
|
|
|
324
357
|
f' {name}: Replaced {namespaced_org_or_c_token} with "{namespaced_org_or_c}"'
|
|
325
358
|
)
|
|
326
359
|
|
|
360
|
+
# Replace new managed_or_namespaced_org token in content
|
|
361
|
+
prev_content = content
|
|
362
|
+
content = content.replace(
|
|
363
|
+
managed_or_namespaced_org_token, managed_or_namespaced_org
|
|
364
|
+
)
|
|
365
|
+
if logger and content != prev_content:
|
|
366
|
+
logger.info(
|
|
367
|
+
f' {name}: Replaced {managed_or_namespaced_org_token} with "{managed_or_namespaced_org}"'
|
|
368
|
+
)
|
|
369
|
+
|
|
327
370
|
# Replace namespace token in file name
|
|
328
371
|
name = name.replace(filename_token, namespace_prefix)
|
|
329
372
|
name = name.replace(namespaced_org_file_token, namespaced_org)
|
|
373
|
+
name = name.replace(managed_or_namespaced_org_file_token, managed_or_namespaced_org)
|
|
330
374
|
if logger and name != orig_name:
|
|
331
375
|
logger.info(f" {orig_name}: renamed to {name}")
|
|
332
376
|
|
|
@@ -737,11 +781,12 @@ def get_tasks_with_options(project_config, frozen_options: frozenset):
|
|
|
737
781
|
coordinator_opts = {}
|
|
738
782
|
options = dict(frozen_options)
|
|
739
783
|
for task_config in project_config.list_tasks():
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
.
|
|
745
|
-
|
|
746
|
-
|
|
784
|
+
task_keys = (
|
|
785
|
+
project_config.get_task(task_config["name"]).get_class().task_options.keys()
|
|
786
|
+
)
|
|
787
|
+
if any(key in options.keys() for key in task_keys):
|
|
788
|
+
# only assign options that are actually used by the task. This is to avoid passing options that are not used by the task.
|
|
789
|
+
coordinator_opts[task_config["name"]] = {
|
|
790
|
+
key: options[key] for key in options.keys() if key in task_keys
|
|
791
|
+
}
|
|
747
792
|
return coordinator_opts
|
cumulusci/utils/classutils.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
from typing import Any, Dict, Set, Type
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_all_subclasses(cls: Type[Any]) -> Set[Type[Any]]:
|
|
2
5
|
"""Return all subclasses of the given class"""
|
|
3
6
|
return set(cls.__subclasses__()).union(
|
|
4
7
|
[s for c in cls.__subclasses__() for s in get_all_subclasses(c)]
|
|
5
8
|
)
|
|
6
9
|
|
|
7
10
|
|
|
8
|
-
def namedtuple_as_simple_dict(self):
|
|
11
|
+
def namedtuple_as_simple_dict(self: Any) -> Dict[str, Any]:
|
|
9
12
|
return {name: getattr(self, name, None) for name in self.__class__._fields}
|
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
interactions:
|
|
2
|
-
- request:
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
2
|
+
- request:
|
|
3
|
+
body: '{"compositeRequest": [{"referenceId": "CCI__RefId__0__", "method": "GET",
|
|
4
|
+
"url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since":
|
|
5
|
+
"Thu, 03 Sep 2020 21:35:07 GMT"}}, {"referenceId": "CCI__RefId__1__", "method":
|
|
6
|
+
"GET", "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since":
|
|
7
|
+
"Thu, 03 Sep 2020 21:35:07 GMT"}}, {"referenceId": "CCI__RefId__2__", "method":
|
|
8
|
+
"GET", "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since":
|
|
9
|
+
"Thu, 03 Sep 2020 21:35:07 GMT"}}]}'
|
|
10
|
+
headers:
|
|
11
|
+
Request-Headers:
|
|
12
|
+
- Elided
|
|
13
|
+
method: POST
|
|
14
|
+
uri: https://orgname.my.salesforce.com/services/data/v49.0/composite
|
|
15
|
+
response:
|
|
16
|
+
body:
|
|
17
|
+
string:
|
|
18
|
+
"{\n \"compositeResponse\" : [ {\n \"body\" : null,\n \"httpHeaders\"
|
|
19
|
+
: {\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri,
|
|
20
|
+
14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\"
|
|
21
|
+
: \"CCI__RefId__0__\"\n }, {\n \"body\" : null,\n \"httpHeaders\" :
|
|
22
|
+
{\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri,
|
|
23
|
+
14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\"
|
|
24
|
+
: \"CCI__RefId__1__\"\n }, {\n \"body\" : null,\n \"httpHeaders\" :
|
|
25
|
+
{\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri,
|
|
26
|
+
14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\"
|
|
27
|
+
: \"CCI__RefId__2__\"\n } ]\n}"
|
|
28
|
+
headers:
|
|
29
|
+
Response-Headers: Elided
|
|
30
|
+
status:
|
|
31
|
+
code: 200
|
|
32
|
+
message: OK
|
|
32
33
|
version: 1
|
cumulusci/utils/options.py
CHANGED
|
@@ -2,7 +2,7 @@ import json
|
|
|
2
2
|
from inspect import signature
|
|
3
3
|
from typing import Any, Dict, List
|
|
4
4
|
|
|
5
|
-
from pydantic import DirectoryPath, Field, FilePath, create_model
|
|
5
|
+
from pydantic.v1 import DirectoryPath, Field, FilePath, create_model
|
|
6
6
|
|
|
7
7
|
from cumulusci.core.exceptions import TaskOptionsError
|
|
8
8
|
from cumulusci.utils.yaml.model_parser import CCIDictModel
|
|
@@ -95,6 +95,8 @@ class ListOfStringsOption(CCIOptionType):
|
|
|
95
95
|
|
|
96
96
|
@classmethod
|
|
97
97
|
def from_str(cls, v) -> List[str]:
|
|
98
|
+
if v is None or v == "":
|
|
99
|
+
return []
|
|
98
100
|
return [s.strip() for s in v.split(",")]
|
|
99
101
|
|
|
100
102
|
|
|
@@ -106,6 +108,26 @@ class MappingOption(CCIOptionType):
|
|
|
106
108
|
return parse_list_of_pairs_dict_arg(v)
|
|
107
109
|
|
|
108
110
|
|
|
111
|
+
class PercentageOption(CCIOptionType):
|
|
112
|
+
"""Parses a percentage from a string in format X%"""
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def validate(cls, v):
|
|
116
|
+
"""Validate and convert a value.
|
|
117
|
+
If its a string, parse it, else, just validate it.
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
return v if isinstance(v, int) else int(v.rstrip("%"))
|
|
121
|
+
except ValueError:
|
|
122
|
+
raise TaskOptionsError(
|
|
123
|
+
"Value should be a percentage or integer (e.g. 90% or 90)"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def from_str(cls, v) -> int:
|
|
128
|
+
return v
|
|
129
|
+
|
|
130
|
+
|
|
109
131
|
def parse_list_of_pairs_dict_arg(arg):
|
|
110
132
|
"""Process an arg in the format "aa:bb,cc:dd" """
|
|
111
133
|
if isinstance(arg, dict):
|
|
@@ -9,8 +9,8 @@ from logging import getLogger
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
11
11
|
|
|
12
|
-
from pydantic import Field, root_validator, validator
|
|
13
|
-
from pydantic.types import DirectoryPath
|
|
12
|
+
from pydantic.v1 import Field, root_validator, validator
|
|
13
|
+
from pydantic.v1.types import DirectoryPath
|
|
14
14
|
from typing_extensions import Literal, TypedDict
|
|
15
15
|
|
|
16
16
|
from cumulusci.core.enums import StrEnum
|
|
@@ -68,6 +68,7 @@ class Task(CCIDictModel):
|
|
|
68
68
|
options: Dict[str, Any] = VSCodeFriendlyDict
|
|
69
69
|
ui_options: Dict[str, Any] = VSCodeFriendlyDict
|
|
70
70
|
name: str = None # get rid of this???
|
|
71
|
+
is_global: bool = False
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
class Flow(CCIDictModel):
|
|
@@ -84,6 +85,11 @@ class Package(CCIDictModel):
|
|
|
84
85
|
uninstall_class: str = None
|
|
85
86
|
api_version: str = None
|
|
86
87
|
metadata_package_id: str = None
|
|
88
|
+
apex_test_access: Optional[dict[str, Union[str, list[str]]]] = None
|
|
89
|
+
package_metadata_access: Optional[dict[str, Union[str, list[str]]]] = None
|
|
90
|
+
unpackaged_metadata_path: Optional[
|
|
91
|
+
Union[str, List[str], Dict[str, Union[str, List[str]]]]
|
|
92
|
+
] = None
|
|
87
93
|
|
|
88
94
|
|
|
89
95
|
class Test(CCIDictModel):
|
|
@@ -321,7 +327,6 @@ class ErrorDict(TypedDict):
|
|
|
321
327
|
|
|
322
328
|
def _log_yaml_errors(logger, errors: List[ErrorDict]):
|
|
323
329
|
"Format and log a Pydantic-style error dictionary"
|
|
324
|
-
# global has_shown_yaml_error_message
|
|
325
330
|
plural = "" if len(errors) <= 1 else "s"
|
|
326
331
|
logger.warning(f"CumulusCI Configuration Warning{plural}:")
|
|
327
332
|
for error in errors:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import IO, Sequence, Union
|
|
3
3
|
|
|
4
|
-
from pydantic import BaseModel, ValidationError
|
|
5
|
-
from pydantic.error_wrappers import ErrorWrapper
|
|
4
|
+
from pydantic.v1 import BaseModel, ValidationError
|
|
5
|
+
from pydantic.v1.error_wrappers import ErrorWrapper
|
|
6
6
|
|
|
7
7
|
from cumulusci.utils.yaml.safer_loader import load_from_source, load_yaml_data
|
|
8
8
|
|
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from unittest.mock import MagicMock, Mock, patch
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
|
-
from pydantic import ValidationError
|
|
7
|
+
from pydantic.v1 import ValidationError
|
|
8
8
|
|
|
9
9
|
from cumulusci.utils import temporary_dir
|
|
10
10
|
from cumulusci.utils.yaml.cumulusci_yml import (
|
|
@@ -2,7 +2,7 @@ from io import StringIO
|
|
|
2
2
|
from unittest.mock import Mock
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
|
-
from pydantic import Field
|
|
5
|
+
from pydantic.v1 import Field
|
|
6
6
|
|
|
7
7
|
from cumulusci.utils.yaml.model_parser import CCIDictModel, CCIModel, ValidationError
|
|
8
8
|
|
|
@@ -20,7 +20,7 @@ class TestCCIModel:
|
|
|
20
20
|
def test_fields_property(self):
|
|
21
21
|
# JSON is YAML. Strange but true.
|
|
22
22
|
foo = Document.parse_from_yaml(StringIO("{bar: 'blah'}"))
|
|
23
|
-
assert
|
|
23
|
+
assert isinstance(foo, Foo)
|
|
24
24
|
assert foo.fields_ == []
|
|
25
25
|
assert foo.fields == []
|
|
26
26
|
|
|
@@ -122,7 +122,7 @@ class TestCCIDictModel:
|
|
|
122
122
|
|
|
123
123
|
# JSON is YAML. Strange but true.
|
|
124
124
|
foo = Document.parse_from_yaml(StringIO("{bar: 'blah'}"))
|
|
125
|
-
assert
|
|
125
|
+
assert isinstance(foo, Foo)
|
|
126
126
|
assert foo["fields"] == []
|
|
127
127
|
|
|
128
128
|
foo = Document.parse_from_yaml(StringIO("{bar: 'blah', fields: [1,2]}"))
|
cumulusci/vcs/base.py
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
|
-
from typing import List, Optional, Type
|
|
3
|
+
from typing import TYPE_CHECKING, List, Optional, Set, Type
|
|
4
4
|
|
|
5
|
-
from cumulusci.core.config import BaseProjectConfig, ServiceConfig
|
|
6
|
-
from cumulusci.core.dependencies.base import DynamicDependency
|
|
7
5
|
from cumulusci.core.keychain import BaseProjectKeychain
|
|
8
|
-
from cumulusci.tasks.release_notes.generator import BaseReleaseNotesGenerator
|
|
9
6
|
from cumulusci.utils.classutils import get_all_subclasses
|
|
10
7
|
from cumulusci.vcs.models import AbstractRelease, AbstractRepo
|
|
11
8
|
from cumulusci.vcs.utils import AbstractCommitDir
|
|
12
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from cumulusci.core.config import BaseProjectConfig, ServiceConfig
|
|
12
|
+
from cumulusci.core.dependencies.base import DynamicDependency
|
|
13
|
+
from cumulusci.tasks.release_notes.generator import BaseReleaseNotesGenerator
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
class VCSService(ABC):
|
|
15
17
|
"""This is an abstract base class for VCS services.
|
|
@@ -18,14 +20,14 @@ class VCSService(ABC):
|
|
|
18
20
|
"""
|
|
19
21
|
|
|
20
22
|
logger: logging.Logger
|
|
21
|
-
config: BaseProjectConfig
|
|
22
|
-
service_config: ServiceConfig
|
|
23
|
+
config: "BaseProjectConfig"
|
|
24
|
+
service_config: "ServiceConfig"
|
|
23
25
|
name: str
|
|
24
26
|
keychain: Optional[BaseProjectKeychain]
|
|
25
27
|
_service_registry: List["VCSService"] = []
|
|
26
28
|
|
|
27
29
|
def __init__(
|
|
28
|
-
self, config: BaseProjectConfig, name: Optional[str] = None, **kwargs
|
|
30
|
+
self, config: "BaseProjectConfig", name: Optional[str] = None, **kwargs
|
|
29
31
|
) -> None:
|
|
30
32
|
"""Initializes the VCS service with the given configuration, service name, and keychain.
|
|
31
33
|
|
|
@@ -35,8 +37,11 @@ class VCSService(ABC):
|
|
|
35
37
|
**kwargs: Additional keyword arguments.
|
|
36
38
|
"""
|
|
37
39
|
self.config = config
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
if config.keychain:
|
|
41
|
+
self.service_config = config.keychain.get_service(self.service_type, name)
|
|
42
|
+
self.name = self.service_config.name
|
|
43
|
+
else:
|
|
44
|
+
self.name = name or ""
|
|
40
45
|
self.keychain = config.keychain
|
|
41
46
|
self.logger = kwargs.get("logger") or logging.getLogger(__name__)
|
|
42
47
|
|
|
@@ -51,11 +56,11 @@ class VCSService(ABC):
|
|
|
51
56
|
raise NotImplementedError(
|
|
52
57
|
"Subclasses should define the service_type property"
|
|
53
58
|
)
|
|
54
|
-
return self.__class__.service_type
|
|
59
|
+
return str(self.__class__.service_type)
|
|
55
60
|
|
|
56
61
|
@property
|
|
57
62
|
@abstractmethod
|
|
58
|
-
def dynamic_dependency_class(self) -> Type[DynamicDependency]:
|
|
63
|
+
def dynamic_dependency_class(self) -> Type["DynamicDependency"]:
|
|
59
64
|
"""Returns the dynamic dependency class for the VCS service.
|
|
60
65
|
This property should be overridden by subclasses to provide
|
|
61
66
|
the specific dynamic dependency class. For example, it could
|
|
@@ -78,7 +83,10 @@ class VCSService(ABC):
|
|
|
78
83
|
@classmethod
|
|
79
84
|
@abstractmethod
|
|
80
85
|
def get_service_for_url(
|
|
81
|
-
cls,
|
|
86
|
+
cls,
|
|
87
|
+
project_config: "BaseProjectConfig",
|
|
88
|
+
url: str,
|
|
89
|
+
service_alias: Optional[str] = None,
|
|
82
90
|
) -> Optional["VCSService"]:
|
|
83
91
|
"""Returns the service configuration for the given URL.
|
|
84
92
|
This method should be overridden by subclasses to provide
|
|
@@ -87,7 +95,7 @@ class VCSService(ABC):
|
|
|
87
95
|
raise NotImplementedError("Subclasses should provide their own implementation")
|
|
88
96
|
|
|
89
97
|
@classmethod
|
|
90
|
-
def registered_services(cls) ->
|
|
98
|
+
def registered_services(cls) -> Set[Type["VCSService"]]:
|
|
91
99
|
"""This method returns all subclasses of VCSService that have been registered.
|
|
92
100
|
It can be used to dynamically discover available VCS services."""
|
|
93
101
|
return get_all_subclasses(cls)
|
|
@@ -127,7 +135,7 @@ class VCSService(ABC):
|
|
|
127
135
|
raise NotImplementedError("Subclasses should provide their own implementation")
|
|
128
136
|
|
|
129
137
|
@abstractmethod
|
|
130
|
-
def release_notes_generator(self, options: dict) -> BaseReleaseNotesGenerator:
|
|
138
|
+
def release_notes_generator(self, options: dict) -> "BaseReleaseNotesGenerator":
|
|
131
139
|
"""Returns the release notes generator for the VCS service."""
|
|
132
140
|
raise NotImplementedError(
|
|
133
141
|
"Subclasses should define the release_notes_generator property"
|
|
@@ -136,7 +144,7 @@ class VCSService(ABC):
|
|
|
136
144
|
@abstractmethod
|
|
137
145
|
def parent_pr_notes_generator(
|
|
138
146
|
self, repo: AbstractRepo
|
|
139
|
-
) -> BaseReleaseNotesGenerator:
|
|
147
|
+
) -> "BaseReleaseNotesGenerator":
|
|
140
148
|
"""Returns the parent PR notes generator for the VCS service."""
|
|
141
149
|
raise NotImplementedError(
|
|
142
150
|
"Subclasses should define the parent_pr_notes_generator property"
|
cumulusci/vcs/bootstrap.py
CHANGED
|
@@ -226,20 +226,21 @@ def get_repo_from_config(config: BaseProjectConfig, options: dict = {}) -> Abstr
|
|
|
226
226
|
|
|
227
227
|
def get_latest_tag(repo: AbstractRepo, beta: bool = False) -> str:
|
|
228
228
|
"""Query Github Releases to find the latest production or beta tag"""
|
|
229
|
-
prefix = repo.project_config.project__git__prefix_release
|
|
229
|
+
prefix = repo.project_config.project__git__prefix_beta if beta else repo.project_config.project__git__prefix_release # type: ignore
|
|
230
230
|
|
|
231
231
|
try:
|
|
232
232
|
if not beta:
|
|
233
233
|
release: Optional[AbstractRelease] = repo.latest_release()
|
|
234
234
|
|
|
235
|
+
if release is None:
|
|
236
|
+
raise
|
|
237
|
+
|
|
235
238
|
if not release.tag_name.startswith(prefix):
|
|
236
239
|
return _get_latest_tag_for_prefix(repo, prefix)
|
|
237
240
|
|
|
238
241
|
return release.tag_name
|
|
239
242
|
else:
|
|
240
|
-
return _get_latest_tag_for_prefix(
|
|
241
|
-
repo, repo.project_config.project__git__prefix_beta
|
|
242
|
-
)
|
|
243
|
+
return _get_latest_tag_for_prefix(repo, prefix)
|
|
243
244
|
except Exception:
|
|
244
245
|
raise VcsException(
|
|
245
246
|
f"No release found for {repo.repo_url} with tag prefix {prefix}"
|