cumulusci-plus 5.0.14__py3-none-any.whl → 5.0.16__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.
Potentially problematic release.
This version of cumulusci-plus might be problematic. Click here for more details.
- cumulusci/__about__.py +1 -1
- cumulusci/cli/cci.py +41 -5
- cumulusci/core/dependencies/base.py +10 -6
- cumulusci/core/dependencies/tests/test_dependencies.py +2 -5
- cumulusci/cumulusci.yml +4 -0
- cumulusci/tasks/utility/env_management.py +157 -0
- cumulusci/tasks/utility/tests/test_env_management.py +220 -0
- cumulusci/utils/__init__.py +16 -0
- {cumulusci_plus-5.0.14.dist-info → cumulusci_plus-5.0.16.dist-info}/METADATA +4 -4
- {cumulusci_plus-5.0.14.dist-info → cumulusci_plus-5.0.16.dist-info}/RECORD +14 -12
- {cumulusci_plus-5.0.14.dist-info → cumulusci_plus-5.0.16.dist-info}/WHEEL +0 -0
- {cumulusci_plus-5.0.14.dist-info → cumulusci_plus-5.0.16.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.14.dist-info → cumulusci_plus-5.0.16.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.14.dist-info → cumulusci_plus-5.0.16.dist-info}/licenses/LICENSE +0 -0
cumulusci/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "5.0.
|
|
1
|
+
__version__ = "5.0.16"
|
cumulusci/cli/cci.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import code
|
|
2
2
|
import contextlib
|
|
3
|
+
import os
|
|
3
4
|
import pdb
|
|
4
5
|
import runpy
|
|
5
6
|
import signal
|
|
@@ -79,8 +80,33 @@ def _cleanup_on_signal(signum):
|
|
|
79
80
|
except Exception as e:
|
|
80
81
|
console.print(f"[red]Error during cleanup: {e}[/red]")
|
|
81
82
|
|
|
82
|
-
#
|
|
83
|
-
|
|
83
|
+
# Terminate child processes in the process group
|
|
84
|
+
try:
|
|
85
|
+
console.print("[yellow]Terminating child processes...[/yellow]")
|
|
86
|
+
|
|
87
|
+
# Temporarily ignore the signal to prevent recursion
|
|
88
|
+
old_handler = signal.signal(signum, signal.SIG_IGN)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
# Only use process group termination on Unix systems
|
|
92
|
+
if hasattr(os, "getpgrp") and hasattr(os, "killpg"):
|
|
93
|
+
pgrp = os.getpgrp()
|
|
94
|
+
# Send signal to all processes in the group except ourselves
|
|
95
|
+
os.killpg(pgrp, signum)
|
|
96
|
+
else:
|
|
97
|
+
# On Windows, we can't use process groups, so just log the attempt
|
|
98
|
+
console.print(
|
|
99
|
+
"[yellow]Process group termination not supported on this platform[/yellow]"
|
|
100
|
+
)
|
|
101
|
+
finally:
|
|
102
|
+
# Restore the original signal handler
|
|
103
|
+
signal.signal(signum, old_handler)
|
|
104
|
+
|
|
105
|
+
except ProcessLookupError:
|
|
106
|
+
# Process group may not exist or may already be terminated
|
|
107
|
+
pass
|
|
108
|
+
except Exception as e:
|
|
109
|
+
console.print(f"[red]Warning: Error terminating child processes: {e}[/red]")
|
|
84
110
|
|
|
85
111
|
# Exit with appropriate failure code
|
|
86
112
|
exit_code = 143 if signum == signal.SIGTERM else 130 # Standard exit codes
|
|
@@ -104,8 +130,15 @@ def main(args=None):
|
|
|
104
130
|
"""
|
|
105
131
|
global _exit_stack
|
|
106
132
|
|
|
107
|
-
#
|
|
108
|
-
#
|
|
133
|
+
# Create a new process group so we can terminate all child processes
|
|
134
|
+
# when we receive a termination signal
|
|
135
|
+
try:
|
|
136
|
+
if hasattr(os, "setpgrp"):
|
|
137
|
+
# On Unix systems, create a new process group
|
|
138
|
+
os.setpgrp()
|
|
139
|
+
except Exception:
|
|
140
|
+
# On Windows or if setpgrp fails, continue without process group
|
|
141
|
+
pass
|
|
109
142
|
|
|
110
143
|
# Set up signal handlers for graceful termination
|
|
111
144
|
signal.signal(signal.SIGTERM, _signal_handler)
|
|
@@ -152,7 +185,10 @@ def main(args=None):
|
|
|
152
185
|
try:
|
|
153
186
|
cli(args[1:], standalone_mode=False, obj=runtime)
|
|
154
187
|
except click.Abort: # Keyboard interrupt
|
|
155
|
-
|
|
188
|
+
if _exit_stack:
|
|
189
|
+
_exit_stack.close()
|
|
190
|
+
Console(stderr=True).print("[yellow]Aborted![/yellow]")
|
|
191
|
+
sys.exit(1)
|
|
156
192
|
except Exception as e:
|
|
157
193
|
if debug:
|
|
158
194
|
console = Console()
|
|
@@ -24,7 +24,11 @@ from cumulusci.core.sfdx import (
|
|
|
24
24
|
from cumulusci.core.utils import format_duration
|
|
25
25
|
from cumulusci.salesforce_api.metadata import ApiDeploy
|
|
26
26
|
from cumulusci.salesforce_api.package_zip import MetadataPackageZipBuilder
|
|
27
|
-
from cumulusci.utils import
|
|
27
|
+
from cumulusci.utils import (
|
|
28
|
+
download_extract_vcs_from_repo,
|
|
29
|
+
get_tasks_with_options,
|
|
30
|
+
temporary_dir,
|
|
31
|
+
)
|
|
28
32
|
from cumulusci.utils.yaml.model_parser import HashableBaseModel
|
|
29
33
|
from cumulusci.utils.ziputils import zip_subfolder
|
|
30
34
|
from cumulusci.vcs.models import AbstractRepo
|
|
@@ -676,15 +680,15 @@ class UnmanagedVcsDependencyFlow(UnmanagedStaticDependency, ABC):
|
|
|
676
680
|
"namespace_strip": self.namespace_strip,
|
|
677
681
|
}
|
|
678
682
|
|
|
683
|
+
coordinator_opts = get_tasks_with_options(
|
|
684
|
+
project_config, frozenset(pre_post_options.items())
|
|
685
|
+
)
|
|
686
|
+
|
|
679
687
|
coordinator = FlowCoordinator(
|
|
680
688
|
project_config,
|
|
681
689
|
flow_config,
|
|
682
690
|
name=flow_config.name,
|
|
683
|
-
options=
|
|
684
|
-
"deploy_pre": pre_post_options,
|
|
685
|
-
"deploy_post": pre_post_options,
|
|
686
|
-
"update_admin_profile": pre_post_options,
|
|
687
|
-
},
|
|
691
|
+
options=coordinator_opts,
|
|
688
692
|
skip=None,
|
|
689
693
|
callbacks=self.callback_class(),
|
|
690
694
|
)
|
|
@@ -1018,6 +1018,7 @@ class TestUnmanagedVcsDependencyFlow:
|
|
|
1018
1018
|
# Setup flow config mock
|
|
1019
1019
|
mock_flow_config = mock.Mock()
|
|
1020
1020
|
mock_project_config.get_flow.return_value = mock_flow_config
|
|
1021
|
+
mock_project_config.list_tasks.return_value = []
|
|
1021
1022
|
|
|
1022
1023
|
# Setup coordinator mock
|
|
1023
1024
|
mock_coordinator_instance = mock.Mock()
|
|
@@ -1076,11 +1077,7 @@ class TestUnmanagedVcsDependencyFlow:
|
|
|
1076
1077
|
assert args[0] == mock_project_config
|
|
1077
1078
|
assert args[1] == mock_flow_config
|
|
1078
1079
|
assert kwargs["name"] == "install_deps"
|
|
1079
|
-
assert kwargs["options"] == {
|
|
1080
|
-
"unmanaged": True, # _get_unmanaged should return True for empty installed_packages
|
|
1081
|
-
"namespace_inject": "test_ns",
|
|
1082
|
-
"namespace_strip": "old_ns",
|
|
1083
|
-
}
|
|
1080
|
+
assert kwargs["options"] == {}
|
|
1084
1081
|
assert kwargs["skip"] is None
|
|
1085
1082
|
assert isinstance(kwargs["callbacks"], type(flow_dep.callback_class()))
|
|
1086
1083
|
|
cumulusci/cumulusci.yml
CHANGED
|
@@ -749,6 +749,10 @@ tasks:
|
|
|
749
749
|
options:
|
|
750
750
|
level: info
|
|
751
751
|
group: Utilities
|
|
752
|
+
configure_env:
|
|
753
|
+
description: Get or set environment variables.
|
|
754
|
+
class_path: cumulusci.tasks.utility.env_management.EnvManagement
|
|
755
|
+
group: Utilities
|
|
752
756
|
download_extract:
|
|
753
757
|
description: Downloads files and folders from a VCS repository.
|
|
754
758
|
class_path: cumulusci.tasks.vcs.download_extract.DownloadExtract
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from datetime import date
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, validator
|
|
8
|
+
|
|
9
|
+
from cumulusci.core.config import BaseProjectConfig, OrgConfig, TaskConfig
|
|
10
|
+
from cumulusci.core.tasks import BaseTask
|
|
11
|
+
from cumulusci.utils.options import CCIOptions, Field
|
|
12
|
+
from cumulusci.vcs.bootstrap import get_repo_from_url
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EnvManagementOption(CCIOptions):
|
|
16
|
+
name: str = Field(
|
|
17
|
+
...,
|
|
18
|
+
description="The name of the environment variable to get the value from the environment",
|
|
19
|
+
)
|
|
20
|
+
default: Any = Field(
|
|
21
|
+
default=None,
|
|
22
|
+
description="The default value of the environment variable. Defaults to None",
|
|
23
|
+
)
|
|
24
|
+
datatype: str = Field(
|
|
25
|
+
default="string",
|
|
26
|
+
description="The datatype of the environment variable. Defaults to string. Valid values are string, bool, int, float, date, list, dict, path, directory, filename, vcs_branch",
|
|
27
|
+
)
|
|
28
|
+
set: bool = Field(
|
|
29
|
+
default=False,
|
|
30
|
+
description="If True, sets the value of the environment variable if it is not already set. Defaults to False",
|
|
31
|
+
)
|
|
32
|
+
url: str = Field(
|
|
33
|
+
default="",
|
|
34
|
+
description="The url of the repository to get the branch value from, Applicable only for vcs_branch datatype. Defaults to empty string",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@validator("datatype")
|
|
38
|
+
def validate_datatype(cls, v):
|
|
39
|
+
if v not in [
|
|
40
|
+
"string",
|
|
41
|
+
"bool",
|
|
42
|
+
"int",
|
|
43
|
+
"float",
|
|
44
|
+
"date",
|
|
45
|
+
"list",
|
|
46
|
+
"dict",
|
|
47
|
+
"path",
|
|
48
|
+
"directory",
|
|
49
|
+
"filename",
|
|
50
|
+
"vcs_branch",
|
|
51
|
+
]:
|
|
52
|
+
raise ValueError(f"Invalid datatype: {v}")
|
|
53
|
+
return v
|
|
54
|
+
|
|
55
|
+
def formated_value(
|
|
56
|
+
self,
|
|
57
|
+
project_config: Optional[BaseProjectConfig],
|
|
58
|
+
org_config: Optional[OrgConfig],
|
|
59
|
+
) -> tuple[Any, str]:
|
|
60
|
+
value = os.getenv(self.name, self.default)
|
|
61
|
+
datatype = self.datatype or "string"
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
match datatype:
|
|
65
|
+
case "string":
|
|
66
|
+
return str(value), str(value)
|
|
67
|
+
case "bool":
|
|
68
|
+
v = DummyValidatorModel(b=value).b
|
|
69
|
+
return v, str(v)
|
|
70
|
+
case "int":
|
|
71
|
+
v = DummyValidatorModel(i=value).i
|
|
72
|
+
return v, str(v)
|
|
73
|
+
case "float":
|
|
74
|
+
v = DummyValidatorModel(f=value).f
|
|
75
|
+
return v, str(v)
|
|
76
|
+
case "date":
|
|
77
|
+
v = DummyValidatorModel(d=date.fromisoformat(str(value))).d
|
|
78
|
+
return v, str(v)
|
|
79
|
+
case "list":
|
|
80
|
+
v = value if isinstance(value, list) else value.split(",")
|
|
81
|
+
return v, str(v)
|
|
82
|
+
case "dict":
|
|
83
|
+
v = value if isinstance(value, dict) else json.loads(str(value))
|
|
84
|
+
return v, str(v)
|
|
85
|
+
case "path":
|
|
86
|
+
v = Path(str(value))
|
|
87
|
+
return v.absolute(), str(v.absolute())
|
|
88
|
+
case "directory":
|
|
89
|
+
v = Path(str(value)).parent.absolute()
|
|
90
|
+
return v, str(v.absolute())
|
|
91
|
+
case "filename":
|
|
92
|
+
v = Path(str(value)).name
|
|
93
|
+
return v, str(v)
|
|
94
|
+
case "vcs_branch":
|
|
95
|
+
task_config = TaskConfig({"options": {"url": self.url}})
|
|
96
|
+
task = VcsRemoteBranch(project_config, task_config, org_config)
|
|
97
|
+
result = task()
|
|
98
|
+
return result["remote_branch"], str(result["remote_branch"])
|
|
99
|
+
case _:
|
|
100
|
+
raise ValueError(f"Invalid datatype: {datatype}")
|
|
101
|
+
except Exception as e:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"Formatting Error: {value} for datatype: {datatype} - {e}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class DummyValidatorModel(BaseModel):
|
|
108
|
+
b: Optional[bool]
|
|
109
|
+
i: Optional[int]
|
|
110
|
+
f: Optional[float]
|
|
111
|
+
d: Optional[date]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class EnvManagement(BaseTask):
|
|
115
|
+
class Options(CCIOptions):
|
|
116
|
+
envs: List[EnvManagementOption] = Field(
|
|
117
|
+
default=[],
|
|
118
|
+
description="A list of environment variables definitions.",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
parsed_options: Options
|
|
122
|
+
|
|
123
|
+
def _run_task(self):
|
|
124
|
+
self.return_values = {}
|
|
125
|
+
|
|
126
|
+
for env_option in self.parsed_options.envs:
|
|
127
|
+
self.return_values[env_option.name], str_value = env_option.formated_value(
|
|
128
|
+
self.project_config, self.org_config
|
|
129
|
+
)
|
|
130
|
+
if env_option.set and env_option.name not in os.environ:
|
|
131
|
+
os.environ[env_option.name] = str_value
|
|
132
|
+
|
|
133
|
+
return self.return_values
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class VcsRemoteBranch(BaseTask):
|
|
137
|
+
class Options(CCIOptions):
|
|
138
|
+
url: str = Field(
|
|
139
|
+
...,
|
|
140
|
+
description="Gets if the remote branch name exist with the same name in the remote repository.",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
parsed_options: Options
|
|
144
|
+
|
|
145
|
+
def _run_task(self):
|
|
146
|
+
self.return_values = {}
|
|
147
|
+
# Get current branch name.
|
|
148
|
+
local_branch = self.project_config.repo_branch
|
|
149
|
+
repo = get_repo_from_url(self.project_config, self.parsed_options.url)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
branch = repo.branch(local_branch)
|
|
153
|
+
self.return_values["remote_branch"] = branch.name
|
|
154
|
+
except Exception:
|
|
155
|
+
self.return_values["remote_branch"] = repo.default_branch
|
|
156
|
+
|
|
157
|
+
return self.return_values
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import unittest
|
|
3
|
+
from datetime import date
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import Mock, patch
|
|
6
|
+
|
|
7
|
+
from cumulusci.core.config import TaskConfig
|
|
8
|
+
from cumulusci.tasks.utility.env_management import (
|
|
9
|
+
EnvManagement,
|
|
10
|
+
EnvManagementOption,
|
|
11
|
+
VcsRemoteBranch,
|
|
12
|
+
)
|
|
13
|
+
from cumulusci.tests.util import create_project_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestVcsRemoteBranch(unittest.TestCase):
|
|
17
|
+
def test_run_task_branch_exists(self):
|
|
18
|
+
project_config = create_project_config()
|
|
19
|
+
project_config.repo_info["branch"] = "feature/branch-1"
|
|
20
|
+
task_config = TaskConfig(
|
|
21
|
+
{"options": {"url": "https://github.com/TestOwner/TestRepo"}}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
with patch(
|
|
25
|
+
"cumulusci.tasks.utility.env_management.get_repo_from_url"
|
|
26
|
+
) as get_repo_mock:
|
|
27
|
+
repo_mock = Mock()
|
|
28
|
+
branch_mock = Mock()
|
|
29
|
+
branch_mock.name = "feature/branch-1"
|
|
30
|
+
repo_mock.branch.return_value = branch_mock
|
|
31
|
+
get_repo_mock.return_value = repo_mock
|
|
32
|
+
|
|
33
|
+
task = VcsRemoteBranch(project_config, task_config)
|
|
34
|
+
task()
|
|
35
|
+
self.assertEqual(task.return_values["remote_branch"], "feature/branch-1")
|
|
36
|
+
repo_mock.branch.assert_called_once_with("feature/branch-1")
|
|
37
|
+
|
|
38
|
+
def test_run_task_branch_not_exist(self):
|
|
39
|
+
project_config = create_project_config()
|
|
40
|
+
project_config.repo_info["branch"] = "feature/branch-1"
|
|
41
|
+
task_config = TaskConfig(
|
|
42
|
+
{"options": {"url": "https://github.com/TestOwner/TestRepo"}}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
with patch(
|
|
46
|
+
"cumulusci.tasks.utility.env_management.get_repo_from_url"
|
|
47
|
+
) as get_repo_mock:
|
|
48
|
+
repo_mock = Mock()
|
|
49
|
+
repo_mock.branch.side_effect = Exception("Branch not found")
|
|
50
|
+
repo_mock.default_branch = "main"
|
|
51
|
+
get_repo_mock.return_value = repo_mock
|
|
52
|
+
|
|
53
|
+
task = VcsRemoteBranch(project_config, task_config)
|
|
54
|
+
task()
|
|
55
|
+
self.assertEqual(task.return_values["remote_branch"], "main")
|
|
56
|
+
repo_mock.branch.assert_called_once_with("feature/branch-1")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestEnvManagement(unittest.TestCase):
|
|
60
|
+
@classmethod
|
|
61
|
+
def setUpClass(cls):
|
|
62
|
+
cls.project_config = create_project_config()
|
|
63
|
+
cls.org_config = Mock()
|
|
64
|
+
|
|
65
|
+
def test_init_options(self):
|
|
66
|
+
task_config = TaskConfig(
|
|
67
|
+
{
|
|
68
|
+
"options": {
|
|
69
|
+
"envs": [
|
|
70
|
+
{"name": "MY_VAR", "default": "default_val"},
|
|
71
|
+
{"name": "MY_BOOL", "datatype": "bool", "default": "true"},
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
task = EnvManagement(self.project_config, task_config)
|
|
77
|
+
self.assertEqual(len(task.parsed_options.envs), 2)
|
|
78
|
+
self.assertIsInstance(task.parsed_options.envs[0], EnvManagementOption)
|
|
79
|
+
|
|
80
|
+
@patch.dict(os.environ, {}, clear=True)
|
|
81
|
+
def test_run_task_get_values(self):
|
|
82
|
+
os.environ["MY_VAR"] = "env_val"
|
|
83
|
+
os.environ["MY_INT"] = "123"
|
|
84
|
+
os.environ["MY_DATE"] = "2023-10-26"
|
|
85
|
+
os.environ["MY_LIST"] = "a,b,c"
|
|
86
|
+
|
|
87
|
+
task_config = TaskConfig(
|
|
88
|
+
{
|
|
89
|
+
"options": {
|
|
90
|
+
"envs": [
|
|
91
|
+
{"name": "MY_VAR"},
|
|
92
|
+
{"name": "MY_INT", "datatype": "int"},
|
|
93
|
+
{"name": "MY_DATE", "datatype": "date"},
|
|
94
|
+
{"name": "MY_LIST", "datatype": "list"},
|
|
95
|
+
{"name": "NOT_SET", "default": "default_val"},
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
task = EnvManagement(self.project_config, task_config, self.org_config)
|
|
101
|
+
result = task()
|
|
102
|
+
|
|
103
|
+
self.assertEqual(
|
|
104
|
+
result,
|
|
105
|
+
{
|
|
106
|
+
"MY_VAR": "env_val",
|
|
107
|
+
"MY_INT": 123,
|
|
108
|
+
"MY_DATE": date(2023, 10, 26),
|
|
109
|
+
"MY_LIST": ["a", "b", "c"],
|
|
110
|
+
"NOT_SET": "default_val",
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
self.assertNotIn("NOT_SET", os.environ)
|
|
114
|
+
|
|
115
|
+
@patch.dict(os.environ, {}, clear=True)
|
|
116
|
+
def test_run_task_set_values(self):
|
|
117
|
+
os.environ["EXISTING_VAR"] = "original_value"
|
|
118
|
+
task_config = TaskConfig(
|
|
119
|
+
{
|
|
120
|
+
"options": {
|
|
121
|
+
"envs": [
|
|
122
|
+
{"name": "NEW_VAR", "default": "new_value", "set": True},
|
|
123
|
+
{
|
|
124
|
+
"name": "EXISTING_VAR",
|
|
125
|
+
"default": "new_default",
|
|
126
|
+
"set": True,
|
|
127
|
+
},
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
task = EnvManagement(self.project_config, task_config, self.org_config)
|
|
133
|
+
task()
|
|
134
|
+
|
|
135
|
+
self.assertEqual(os.environ.get("NEW_VAR"), "new_value")
|
|
136
|
+
self.assertEqual(os.environ.get("EXISTING_VAR"), "original_value")
|
|
137
|
+
|
|
138
|
+
def test_datatype_validation(self):
|
|
139
|
+
with self.assertRaises(ValueError):
|
|
140
|
+
EnvManagementOption(name="test", datatype="invalid")
|
|
141
|
+
|
|
142
|
+
@patch.dict(os.environ, {}, clear=True)
|
|
143
|
+
def _test_datatype_conversion(self, datatype, env_value, expected_value):
|
|
144
|
+
os.environ["TEST_VAR"] = str(env_value)
|
|
145
|
+
task_config = TaskConfig(
|
|
146
|
+
{"options": {"envs": [{"name": "TEST_VAR", "datatype": datatype}]}}
|
|
147
|
+
)
|
|
148
|
+
task = EnvManagement(self.project_config, task_config, self.org_config)
|
|
149
|
+
result = task()
|
|
150
|
+
self.assertEqual(result["TEST_VAR"], expected_value)
|
|
151
|
+
|
|
152
|
+
def test_datatypes(self):
|
|
153
|
+
test_cases = [
|
|
154
|
+
("string", "hello", "hello"),
|
|
155
|
+
("bool", "true", True),
|
|
156
|
+
("bool", "0", False),
|
|
157
|
+
("int", "42", 42),
|
|
158
|
+
("float", "3.14", 3.14),
|
|
159
|
+
("date", "2024-01-01", date(2024, 1, 1)),
|
|
160
|
+
("list", "one,two", ["one", "two"]),
|
|
161
|
+
("dict", '{"key": "value"}', {"key": "value"}),
|
|
162
|
+
("path", "/tmp/test", Path("/tmp/test").absolute()),
|
|
163
|
+
("directory", "/tmp/test/file.txt", Path("/tmp/test").absolute()),
|
|
164
|
+
("filename", "/tmp/test/file.txt", "file.txt"),
|
|
165
|
+
]
|
|
166
|
+
for datatype, env_value, expected_value in test_cases:
|
|
167
|
+
with self.subTest(datatype=datatype):
|
|
168
|
+
self._test_datatype_conversion(datatype, env_value, expected_value)
|
|
169
|
+
|
|
170
|
+
def test_formatting_error(self):
|
|
171
|
+
os.environ["TEST_VAR"] = "not-an-int"
|
|
172
|
+
task_config = TaskConfig(
|
|
173
|
+
{"options": {"envs": [{"name": "TEST_VAR", "datatype": "int"}]}}
|
|
174
|
+
)
|
|
175
|
+
task = EnvManagement(self.project_config, task_config, self.org_config)
|
|
176
|
+
with self.assertRaises(ValueError):
|
|
177
|
+
task()
|
|
178
|
+
|
|
179
|
+
@patch("cumulusci.tasks.utility.env_management.VcsRemoteBranch")
|
|
180
|
+
def test_vcs_branch_datatype(self, vcs_mock):
|
|
181
|
+
os.environ["TEST_VAR"] = "" # vcs_branch doesn't use env var
|
|
182
|
+
|
|
183
|
+
vcs_instance_mock = vcs_mock.return_value
|
|
184
|
+
vcs_instance_mock.return_value = {"remote_branch": "my-feature-branch"}
|
|
185
|
+
|
|
186
|
+
task_config = TaskConfig(
|
|
187
|
+
{
|
|
188
|
+
"options": {
|
|
189
|
+
"envs": [
|
|
190
|
+
{
|
|
191
|
+
"name": "VCS_BRANCH",
|
|
192
|
+
"datatype": "vcs_branch",
|
|
193
|
+
"url": "https://github.com/TestOwner/TestRepo",
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
task = EnvManagement(self.project_config, task_config, self.org_config)
|
|
200
|
+
result = task()
|
|
201
|
+
|
|
202
|
+
self.assertEqual(result["VCS_BRANCH"], "my-feature-branch")
|
|
203
|
+
vcs_mock.assert_called_once()
|
|
204
|
+
|
|
205
|
+
@patch.dict(os.environ, {}, clear=True)
|
|
206
|
+
def test_run_task_with_default_types(self):
|
|
207
|
+
task_config = TaskConfig(
|
|
208
|
+
{
|
|
209
|
+
"options": {
|
|
210
|
+
"envs": [
|
|
211
|
+
{"name": "MY_LIST", "datatype": "list", "default": ["a", "b"]},
|
|
212
|
+
{"name": "MY_DICT", "datatype": "dict", "default": {"x": 1}},
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
task = EnvManagement(self.project_config, task_config, self.org_config)
|
|
218
|
+
result = task()
|
|
219
|
+
self.assertEqual(result["MY_LIST"], ["a", "b"])
|
|
220
|
+
self.assertEqual(result["MY_DICT"], {"x": 1})
|
cumulusci/utils/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ import sys
|
|
|
9
9
|
import tempfile
|
|
10
10
|
import textwrap
|
|
11
11
|
import zipfile
|
|
12
|
+
import functools
|
|
12
13
|
from datetime import datetime
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Union, Optional
|
|
@@ -729,3 +730,18 @@ def filter_namelist(includes, namelist):
|
|
|
729
730
|
if name.startswith(tuple(included_dirs)) or name in includes
|
|
730
731
|
}
|
|
731
732
|
)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
@functools.lru_cache(50)
|
|
736
|
+
def get_tasks_with_options(project_config, frozen_options: frozenset):
|
|
737
|
+
coordinator_opts = {}
|
|
738
|
+
options = dict(frozen_options)
|
|
739
|
+
for task_config in project_config.list_tasks():
|
|
740
|
+
if any(
|
|
741
|
+
key in options.keys()
|
|
742
|
+
for key in project_config.get_task(task_config["name"])
|
|
743
|
+
.get_class()
|
|
744
|
+
.task_options.keys()
|
|
745
|
+
):
|
|
746
|
+
coordinator_opts[task_config["name"]] = options
|
|
747
|
+
return coordinator_opts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cumulusci-plus
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.16
|
|
4
4
|
Summary: Build and release tools for Salesforce developers
|
|
5
5
|
Project-URL: Homepage, https://github.com/jorgesolebur/CumulusCI
|
|
6
6
|
Project-URL: Changelog, https://cumulusci.readthedocs.io/en/stable/history.html
|
|
@@ -127,7 +127,7 @@ license](https://github.com/SFDO-Tooling/CumulusCI/blob/main/LICENSE)
|
|
|
127
127
|
and is not covered by the Salesforce Master Subscription Agreement.
|
|
128
128
|
|
|
129
129
|
<!-- Changelog -->
|
|
130
|
-
## v5.0.
|
|
130
|
+
## v5.0.16 (2025-08-20)
|
|
131
131
|
|
|
132
132
|
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
|
133
133
|
|
|
@@ -135,6 +135,6 @@ and is not covered by the Salesforce Master Subscription Agreement.
|
|
|
135
135
|
|
|
136
136
|
### Changes
|
|
137
137
|
|
|
138
|
-
-
|
|
138
|
+
- Add configure_env task and fix failing test classes. by [@rupeshjSFDC](https://github.com/rupeshjSFDC) in [#57](https://github.com/jorgesolebur/CumulusCI/pull/57)
|
|
139
139
|
|
|
140
|
-
**Full Changelog**: https://github.com/jorgesolebur/CumulusCI/compare/v5.0.
|
|
140
|
+
**Full Changelog**: https://github.com/jorgesolebur/CumulusCI/compare/v5.0.15...v5.0.16
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
cumulusci/__about__.py,sha256=
|
|
1
|
+
cumulusci/__about__.py,sha256=6Zfa5_jynCBYIvtkYN0Ygch4VWXnobnaeed612gFG18,23
|
|
2
2
|
cumulusci/__init__.py,sha256=jdanFQ_i8vbdO7Eltsf4pOfvV4mwa_Osyc4gxWKJ8ng,764
|
|
3
3
|
cumulusci/__main__.py,sha256=kgRH-n5AJrH_daCK_EJwH7azAUxdXEmpi-r-dPGMR6Y,43
|
|
4
4
|
cumulusci/conftest.py,sha256=AIL98BDwNAQtdo8YFmLKwav0tmrQ5dpbw1cX2FyGouQ,5108
|
|
5
|
-
cumulusci/cumulusci.yml,sha256=
|
|
5
|
+
cumulusci/cumulusci.yml,sha256=h4Ywck_HV5PXqjdTvA6jMm8dxPLZi2tmFdwGmssnhdo,72617
|
|
6
6
|
cumulusci/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
cumulusci/cli/cci.py,sha256=
|
|
7
|
+
cumulusci/cli/cci.py,sha256=yAq8jFoGde6g_1TeAAjzZYsk77itiONCQGBFe3g3nOs,11836
|
|
8
8
|
cumulusci/cli/error.py,sha256=znj0YN8D2Grozm1u7mZAsJlmmdGebbuy0c1ofQluL4Q,4410
|
|
9
9
|
cumulusci/cli/flow.py,sha256=rN_9WL2Z6dcx-oRngChIgei3E5Qmg3XVzk5ND1o0i3s,6171
|
|
10
10
|
cumulusci/cli/logger.py,sha256=bpzSD0Bm0BAwdNbVR6yZXMREh2vm7jOytZevEaNoVR4,2267
|
|
@@ -65,7 +65,7 @@ cumulusci/core/config/tests/test_config.py,sha256=ZtIQSIzQWebw7mYXgehnp3CvoatC_t
|
|
|
65
65
|
cumulusci/core/config/tests/test_config_expensive.py,sha256=__3JEuoAQ8s5njTcbyZlpXHr0jR0Qtne96xyF7fzqjQ,30137
|
|
66
66
|
cumulusci/core/config/tests/test_config_util.py,sha256=X1SY9PIhLoQuC8duBKgs804aghN3n12DhqiC_f6jSmM,3177
|
|
67
67
|
cumulusci/core/dependencies/__init__.py,sha256=Txf4VCrRW-aREKHqzK3ZyauQMsgtCXjiLkQzpMQT0kI,1533
|
|
68
|
-
cumulusci/core/dependencies/base.py,sha256=
|
|
68
|
+
cumulusci/core/dependencies/base.py,sha256=JBLma_3fB5q-ee5zs-WpSX6hfbGJ09UYgs6vquROjiQ,23433
|
|
69
69
|
cumulusci/core/dependencies/dependencies.py,sha256=74KjrCRn7AjKPBSGOm4pU34eJd8GSp1wNs7EFIC0wBE,8689
|
|
70
70
|
cumulusci/core/dependencies/github.py,sha256=ozpRc5ADJsRDD5C_T-TLFygnBDE5Y9_03ZLCtZ-qr98,5897
|
|
71
71
|
cumulusci/core/dependencies/github_resolvers.py,sha256=Em8p41Q-npoKv1ZAYNxXVrluQmYitzVfLLXlmln-MGw,9196
|
|
@@ -73,7 +73,7 @@ cumulusci/core/dependencies/resolvers.py,sha256=xWVijK6Eu-WFyGnQPFANLkZFTjq4NQYh
|
|
|
73
73
|
cumulusci/core/dependencies/utils.py,sha256=y54fWqLZ8IUIV9i1b6WcDXIJsK0ygny5DTsZCXbgeoM,369
|
|
74
74
|
cumulusci/core/dependencies/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
75
75
|
cumulusci/core/dependencies/tests/conftest.py,sha256=rd0ZqpV6GTk7IRtPYJ7jyLWHM1g_xtZ4PABuvkN-TZY,11004
|
|
76
|
-
cumulusci/core/dependencies/tests/test_dependencies.py,sha256=
|
|
76
|
+
cumulusci/core/dependencies/tests/test_dependencies.py,sha256=1nE1dkhwOSWyIP3KWyy5GJrUa-DMDOGcoWxcoSRV3D0,40572
|
|
77
77
|
cumulusci/core/dependencies/tests/test_github.py,sha256=_JiUh-xgGmnsA2X_57i3gOwcDv_xBj66mdhitcZ-APU,2765
|
|
78
78
|
cumulusci/core/dependencies/tests/test_resolvers.py,sha256=kLnvs0QTDKQwkxZ_lAAcetWZUNwBoP-TJYp9fdgRbHU,36271
|
|
79
79
|
cumulusci/core/keychain/__init__.py,sha256=UbuaIrKZSczVVqbG_7BHFTkQukbqinGJlJlQGIpIsOI,595
|
|
@@ -611,6 +611,8 @@ cumulusci/tasks/tests/test_pushfails.py,sha256=9JG9D0iD4dR-1fKheaRN7BEy3lzzuOKeR
|
|
|
611
611
|
cumulusci/tasks/tests/test_salesforce.py,sha256=yCGtuHapxyAEmXQhuF2g2fh2naknTu7Md4OfEJQvGAA,2594
|
|
612
612
|
cumulusci/tasks/tests/test_sfdx.py,sha256=oUbHo28d796m5RuskXMLitJw2rCLjjXIfxggzr4gsso,3545
|
|
613
613
|
cumulusci/tasks/tests/test_util.py,sha256=D1T0QnvPTS0PHeZWo2xiVgE1jVTYcLzGTGHwEIoVmxk,7296
|
|
614
|
+
cumulusci/tasks/utility/env_management.py,sha256=RIxciQHPVMnJjwNvYkeq30QrMISagF4qx2zSRiXjExQ,5382
|
|
615
|
+
cumulusci/tasks/utility/tests/test_env_management.py,sha256=e0cVAohNzpDUPDvS5LrIcd2Cqv0pkQ_KYvjX6qsREj8,8155
|
|
614
616
|
cumulusci/tasks/vcs/__init__.py,sha256=ZzpMZnhooXZ6r_ywBVTS3UNw9uMcXW6h33LylRqTDK0,700
|
|
615
617
|
cumulusci/tasks/vcs/commit_status.py,sha256=hgPUVHeQyIfMsCuwrw2RI-ufnbjdRARc6HI3BEgdcxI,2332
|
|
616
618
|
cumulusci/tasks/vcs/create_commit_status.py,sha256=kr_OFM3J32jxusS1og-K91Kl1F_Nr7SlevRsJKBhifs,1565
|
|
@@ -663,7 +665,7 @@ cumulusci/tests/shared_cassettes/vcr_string_templates/batchInfoList_xml.tpl,sha2
|
|
|
663
665
|
cumulusci/tests/shared_cassettes/vcr_string_templates/batchInfo_xml.tpl,sha256=QVliMmBsCaTLx6ichcTSJEk58ejkNmKQfYchr8a62kE,553
|
|
664
666
|
cumulusci/tests/shared_cassettes/vcr_string_templates/jobInfo_insert_xml.tpl,sha256=MELjgmEaKPEVMcZNO3s8fn5QjvN5qU7F1svg8tc5yoA,1113
|
|
665
667
|
cumulusci/tests/shared_cassettes/vcr_string_templates/jobInfo_upsert_xml.tpl,sha256=Pk2twM954A_jndsGJXLhe1S00Sd64O6a2QNlnfj83Dw,1152
|
|
666
|
-
cumulusci/utils/__init__.py,sha256=
|
|
668
|
+
cumulusci/utils/__init__.py,sha256=5_gbnBhl1MtXqzVT-8fhM9rA1eZ4811sk5wHuJWv5_I,25639
|
|
667
669
|
cumulusci/utils/classutils.py,sha256=paXwI8ZkwLr0Wv2fhs99GXh4Jhoinye6m1UJPPrtJFM,322
|
|
668
670
|
cumulusci/utils/collections.py,sha256=4XcsBCyDhNcxSL589Y6gjNONKeB7YLqQ-nlWmiZjIlo,691
|
|
669
671
|
cumulusci/utils/deprecation.py,sha256=MDQ4kbAS3RhpVAA_WVCXRMDDEjJj3pq9eeBDyPNcEoc,216
|
|
@@ -736,9 +738,9 @@ cumulusci/vcs/tests/dummy_service.py,sha256=RltOUpMIhSDNrfxk0LhLqlH4ppC0sK6NC2cO
|
|
|
736
738
|
cumulusci/vcs/tests/test_vcs_base.py,sha256=9mp6uZ3lTxY4onjUNCucp9N9aB3UylKS7_2Zu_hdAZw,24331
|
|
737
739
|
cumulusci/vcs/tests/test_vcs_bootstrap.py,sha256=N0NA48-rGNIIjY3Z7PtVnNwHObSlEGDk2K55TQGI8g4,27954
|
|
738
740
|
cumulusci/vcs/utils/__init__.py,sha256=py4fEcHM7Vd0M0XWznOlywxaeCtG3nEVGmELmEKVGU8,869
|
|
739
|
-
cumulusci_plus-5.0.
|
|
740
|
-
cumulusci_plus-5.0.
|
|
741
|
-
cumulusci_plus-5.0.
|
|
742
|
-
cumulusci_plus-5.0.
|
|
743
|
-
cumulusci_plus-5.0.
|
|
744
|
-
cumulusci_plus-5.0.
|
|
741
|
+
cumulusci_plus-5.0.16.dist-info/METADATA,sha256=1w8L_cNeKkI7tHu_r8PY7nltM-adAck3ZpmjDqihHCk,5773
|
|
742
|
+
cumulusci_plus-5.0.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
743
|
+
cumulusci_plus-5.0.16.dist-info/entry_points.txt,sha256=nTtu04b9iLXhzADcTrb5PwmdXE6e2MTUAMh9OK6Z2pg,80
|
|
744
|
+
cumulusci_plus-5.0.16.dist-info/licenses/AUTHORS.rst,sha256=PvewjKImdKPhhJ6xR2EEZ4T7GbpY2ZeAeyWm2aLtiMQ,676
|
|
745
|
+
cumulusci_plus-5.0.16.dist-info/licenses/LICENSE,sha256=NFsF_s7RVXk2dU6tmRAN8wF45pnD98VZ5IwqOsyBcaU,1499
|
|
746
|
+
cumulusci_plus-5.0.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|