cumulusci-plus 5.0.16__py3-none-any.whl → 5.0.18__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/cumulusci.yml +16 -0
- cumulusci/tasks/salesforce/SfDataCommands.py +166 -0
- cumulusci/tasks/salesforce/tests/test_SfDataCommands.py +363 -0
- cumulusci/tasks/utility/data_management.py +16 -0
- cumulusci/tasks/utility/env_management.py +82 -53
- cumulusci/tasks/utility/tests/test_data_management.py +43 -0
- cumulusci/tasks/utility/tests/test_env_management.py +30 -12
- {cumulusci_plus-5.0.16.dist-info → cumulusci_plus-5.0.18.dist-info}/METADATA +4 -4
- {cumulusci_plus-5.0.16.dist-info → cumulusci_plus-5.0.18.dist-info}/RECORD +14 -10
- {cumulusci_plus-5.0.16.dist-info → cumulusci_plus-5.0.18.dist-info}/WHEEL +0 -0
- {cumulusci_plus-5.0.16.dist-info → cumulusci_plus-5.0.18.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.16.dist-info → cumulusci_plus-5.0.18.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.16.dist-info → cumulusci_plus-5.0.18.dist-info}/licenses/LICENSE +0 -0
cumulusci/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "5.0.
|
|
1
|
+
__version__ = "5.0.18"
|
cumulusci/cumulusci.yml
CHANGED
|
@@ -811,6 +811,22 @@ tasks:
|
|
|
811
811
|
class_path: cumulusci.tasks.vlocity.vlocity.VlocityDeployTask
|
|
812
812
|
description: "Executes the `vlocity packDeploy` command against an org"
|
|
813
813
|
group: OmniStudio
|
|
814
|
+
data_query:
|
|
815
|
+
class_path: cumulusci.tasks.salesforce.SfDataCommands.DataQueryTask
|
|
816
|
+
description: "Executes the `sf data query` command against an org"
|
|
817
|
+
group: SalesforceDX Data Commands
|
|
818
|
+
data_delete:
|
|
819
|
+
class_path: cumulusci.tasks.salesforce.SfDataCommands.DataDeleteRecordTask
|
|
820
|
+
description: "Executes the `sf data delete` command against an org"
|
|
821
|
+
group: SalesforceDX Data Commands
|
|
822
|
+
data_create:
|
|
823
|
+
class_path: cumulusci.tasks.salesforce.SfDataCommands.DataCreateRecordTask
|
|
824
|
+
description: "Executes the `sf data create` command against an org"
|
|
825
|
+
group: SalesforceDX Data Commands
|
|
826
|
+
get_first_item_from_record_list:
|
|
827
|
+
class_path: cumulusci.tasks.utility.data_management.GetFirstItemFromRecordListTask
|
|
828
|
+
description: "Get the first item from a record list"
|
|
829
|
+
group: Utilities
|
|
814
830
|
flows:
|
|
815
831
|
ci_beta:
|
|
816
832
|
group: Continuous Integration
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import sarge
|
|
4
|
+
|
|
5
|
+
from cumulusci.core.exceptions import SalesforceDXException
|
|
6
|
+
from cumulusci.core.sfdx import sfdx
|
|
7
|
+
from cumulusci.tasks.salesforce import BaseSalesforceApiTask
|
|
8
|
+
from cumulusci.utils.options import CCIOptions, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SfDataCommands(BaseSalesforceApiTask):
|
|
12
|
+
class Options(CCIOptions):
|
|
13
|
+
json_output: bool = Field(
|
|
14
|
+
None, description="Whether to return the result as a JSON object"
|
|
15
|
+
)
|
|
16
|
+
api_version: str = Field(None, description="API version to use for the command")
|
|
17
|
+
flags_dir: str = Field(None, description="Import flag values from a directory")
|
|
18
|
+
|
|
19
|
+
parsed_options: Options
|
|
20
|
+
|
|
21
|
+
def _init_task(self):
|
|
22
|
+
super()._init_task()
|
|
23
|
+
|
|
24
|
+
def _init_options(self, kwargs):
|
|
25
|
+
self.args = []
|
|
26
|
+
self.data_command = "data "
|
|
27
|
+
super()._init_options(kwargs)
|
|
28
|
+
if self.parsed_options.flags_dir:
|
|
29
|
+
self.args.extend(["--flags-dir ", self.parsed_options.flags_dir])
|
|
30
|
+
if self.parsed_options.json_output:
|
|
31
|
+
self.args.extend(["--json"])
|
|
32
|
+
if self.parsed_options.api_version:
|
|
33
|
+
self.args.extend(["--api_version", self.parsed_options.api_version])
|
|
34
|
+
|
|
35
|
+
def _run_task(self):
|
|
36
|
+
self.return_values = {}
|
|
37
|
+
|
|
38
|
+
self.p: sarge.Command = sfdx(
|
|
39
|
+
self.data_command,
|
|
40
|
+
log_note="Running data command",
|
|
41
|
+
args=self.args,
|
|
42
|
+
check_return=True,
|
|
43
|
+
username=self.org_config.username,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if self.parsed_options.json_output:
|
|
47
|
+
self.return_values = self._load_json_output(self.p)
|
|
48
|
+
|
|
49
|
+
for line in self.p.stdout_text:
|
|
50
|
+
self.logger.info(line)
|
|
51
|
+
|
|
52
|
+
for line in self.p.stderr_text:
|
|
53
|
+
self.logger.error(line)
|
|
54
|
+
|
|
55
|
+
def _load_json_output(self, p: sarge.Command, stdout: str = None):
|
|
56
|
+
try:
|
|
57
|
+
stdout = stdout or p.stdout_text.read()
|
|
58
|
+
return json.loads(stdout)
|
|
59
|
+
except json.decoder.JSONDecodeError:
|
|
60
|
+
raise SalesforceDXException(
|
|
61
|
+
f"Failed to parse the output of the {self.data_command} command"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SfDataToolingAPISupportedCommands(SfDataCommands):
|
|
66
|
+
class Options(SfDataCommands.Options):
|
|
67
|
+
use_tooling_api: bool = Field(
|
|
68
|
+
None,
|
|
69
|
+
description="Use Tooling API so you can run queries on Tooling API objects.",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DataQueryTask(SfDataToolingAPISupportedCommands):
|
|
74
|
+
class Options(SfDataToolingAPISupportedCommands.Options):
|
|
75
|
+
query: str = Field(None, description="SOQL query to execute")
|
|
76
|
+
file: str = Field(None, description="File that contains the SOQL query")
|
|
77
|
+
all_rows: bool = Field(
|
|
78
|
+
None,
|
|
79
|
+
description="Include deleted records. By default, deleted records are not returned.",
|
|
80
|
+
)
|
|
81
|
+
result_format: str = Field(
|
|
82
|
+
None,
|
|
83
|
+
description="Format to display the results; the --json_output flag overrides this flag. Permissible values are: human, csv, json.",
|
|
84
|
+
)
|
|
85
|
+
output_file: str = Field(
|
|
86
|
+
None,
|
|
87
|
+
description="File where records are written; only CSV and JSON output formats are supported.",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _init_task(self):
|
|
91
|
+
super()._init_task()
|
|
92
|
+
self.data_command += "query"
|
|
93
|
+
|
|
94
|
+
def _init_options(self, kwargs):
|
|
95
|
+
super()._init_options(kwargs)
|
|
96
|
+
if self.parsed_options.query:
|
|
97
|
+
self.args.extend(["--query", self.parsed_options.query])
|
|
98
|
+
if self.parsed_options.file:
|
|
99
|
+
self.args.extend(["--file", self.parsed_options.file])
|
|
100
|
+
if self.parsed_options.all_rows:
|
|
101
|
+
self.args.extend(["--all-rows", self.parsed_options.all_rows])
|
|
102
|
+
if self.parsed_options.result_format:
|
|
103
|
+
self.args.extend(["--result-format", self.parsed_options.result_format])
|
|
104
|
+
if self.parsed_options.output_file:
|
|
105
|
+
self.args.extend(["--output-file", self.parsed_options.output_file])
|
|
106
|
+
|
|
107
|
+
def _run_task(self):
|
|
108
|
+
super()._run_task()
|
|
109
|
+
|
|
110
|
+
if self.parsed_options.json_output:
|
|
111
|
+
self.logger.info(self.return_values)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class DataCreateRecordTask(SfDataToolingAPISupportedCommands):
|
|
115
|
+
class Options(SfDataToolingAPISupportedCommands.Options):
|
|
116
|
+
sobject: str = Field(
|
|
117
|
+
...,
|
|
118
|
+
description="API name of the Salesforce or Tooling API object that you're inserting a record into.",
|
|
119
|
+
)
|
|
120
|
+
values: str = Field(
|
|
121
|
+
...,
|
|
122
|
+
description="Values for the flags in the form <fieldName>=<value>, separate multiple pairs with spaces.",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _init_task(self):
|
|
126
|
+
super()._init_task()
|
|
127
|
+
self.data_command += "create record"
|
|
128
|
+
|
|
129
|
+
def _init_options(self, kwargs):
|
|
130
|
+
super()._init_options(kwargs)
|
|
131
|
+
if self.options.get("sobject"):
|
|
132
|
+
self.args.extend(["--sobject", self.options.get("sobject")])
|
|
133
|
+
if self.options.get("values"):
|
|
134
|
+
self.args.extend(["--values", self.options.get("values")])
|
|
135
|
+
|
|
136
|
+
def _run_task(self):
|
|
137
|
+
return super()._run_task()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class DataDeleteRecordTask(SfDataToolingAPISupportedCommands):
|
|
141
|
+
class Options(SfDataToolingAPISupportedCommands.Options):
|
|
142
|
+
sobject: str = Field(
|
|
143
|
+
...,
|
|
144
|
+
description="API name of the Salesforce or Tooling API object that you're deleting a record from.",
|
|
145
|
+
)
|
|
146
|
+
record_id: str = Field(None, description="ID of the record you’re deleting.")
|
|
147
|
+
where: str = Field(
|
|
148
|
+
None,
|
|
149
|
+
description="List of <fieldName>=<value> pairs that identify the record you want to delete.",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def _init_task(self):
|
|
153
|
+
super()._init_task()
|
|
154
|
+
self.data_command += "delete record"
|
|
155
|
+
|
|
156
|
+
def _init_options(self, kwargs):
|
|
157
|
+
super()._init_options(kwargs)
|
|
158
|
+
if self.options.get("sobject"):
|
|
159
|
+
self.args.extend(["--sobject", self.options.get("sobject")])
|
|
160
|
+
if self.options.get("record_id"):
|
|
161
|
+
self.args.extend(["--record-id", self.options.get("record_id")])
|
|
162
|
+
if self.options.get("where"):
|
|
163
|
+
self.args.extend(["--where", self.options.get("where")])
|
|
164
|
+
|
|
165
|
+
def _run_task(self):
|
|
166
|
+
return super()._run_task()
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
import sarge
|
|
6
|
+
|
|
7
|
+
from cumulusci.core.exceptions import SalesforceDXException
|
|
8
|
+
from cumulusci.tasks.salesforce.SfDataCommands import (
|
|
9
|
+
DataCreateRecordTask,
|
|
10
|
+
DataDeleteRecordTask,
|
|
11
|
+
DataQueryTask,
|
|
12
|
+
SfDataCommands,
|
|
13
|
+
SfDataToolingAPISupportedCommands,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from . import create_task
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_mock_sarge_command(stdout="", stderr="", returncode=0):
|
|
20
|
+
"""Create a mock sarge.Command that satisfies type checking"""
|
|
21
|
+
mock_command = mock.Mock(spec=sarge.Command)
|
|
22
|
+
mock_command.returncode = returncode
|
|
23
|
+
|
|
24
|
+
# Mock stdout_text
|
|
25
|
+
stdout_lines = stdout.split("\n") if stdout else []
|
|
26
|
+
mock_stdout_text = mock.Mock()
|
|
27
|
+
mock_stdout_text.__iter__ = mock.Mock(return_value=iter(stdout_lines))
|
|
28
|
+
mock_stdout_text.read = mock.Mock(return_value=stdout)
|
|
29
|
+
mock_command.stdout_text = mock_stdout_text
|
|
30
|
+
|
|
31
|
+
# Mock stderr_text
|
|
32
|
+
stderr_lines = stderr.split("\n") if stderr else []
|
|
33
|
+
mock_stderr_text = mock.Mock()
|
|
34
|
+
mock_stderr_text.__iter__ = mock.Mock(return_value=iter(stderr_lines))
|
|
35
|
+
mock_stderr_text.read = mock.Mock(return_value=stderr)
|
|
36
|
+
mock_command.stderr_text = mock_stderr_text
|
|
37
|
+
|
|
38
|
+
return mock_command
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@mock.patch("cumulusci.tasks.salesforce.SfDataCommands.sfdx")
|
|
42
|
+
class TestSfDataCommands:
|
|
43
|
+
def test_init_options_basic(self, mock_sfdx):
|
|
44
|
+
mock_sfdx.return_value = create_mock_sarge_command()
|
|
45
|
+
task = create_task(SfDataCommands, {})
|
|
46
|
+
task()
|
|
47
|
+
assert task.data_command == "data "
|
|
48
|
+
assert task.args == []
|
|
49
|
+
|
|
50
|
+
def test_init_options_all_flags(self, mock_sfdx):
|
|
51
|
+
json_response = {"status": 0, "result": {}}
|
|
52
|
+
mock_sfdx.return_value = create_mock_sarge_command(
|
|
53
|
+
stdout=json.dumps(json_response)
|
|
54
|
+
)
|
|
55
|
+
task = create_task(
|
|
56
|
+
SfDataCommands,
|
|
57
|
+
{
|
|
58
|
+
"json_output": True,
|
|
59
|
+
"api_version": "50.0",
|
|
60
|
+
"flags_dir": "/tmp/flags",
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
task()
|
|
64
|
+
expected_args = [
|
|
65
|
+
"--flags-dir ",
|
|
66
|
+
"/tmp/flags",
|
|
67
|
+
"--json",
|
|
68
|
+
"--api_version",
|
|
69
|
+
"50.0",
|
|
70
|
+
]
|
|
71
|
+
assert all(arg in task.args for arg in expected_args)
|
|
72
|
+
|
|
73
|
+
def test_load_json_output_success(self, mock_sfdx):
|
|
74
|
+
json_response = {"status": 0, "result": {"records": []}}
|
|
75
|
+
stdout = json.dumps(json_response)
|
|
76
|
+
mock_command = create_mock_sarge_command(stdout=stdout)
|
|
77
|
+
mock_sfdx.return_value = mock_command
|
|
78
|
+
|
|
79
|
+
task = create_task(SfDataCommands, {"json_output": True})
|
|
80
|
+
task()
|
|
81
|
+
|
|
82
|
+
assert task.return_values == json_response
|
|
83
|
+
|
|
84
|
+
def test_load_json_output_decode_error(self, mock_sfdx):
|
|
85
|
+
mock_command = create_mock_sarge_command(stdout="invalid json")
|
|
86
|
+
mock_sfdx.return_value = mock_command
|
|
87
|
+
|
|
88
|
+
task = create_task(SfDataCommands, {"json_output": True})
|
|
89
|
+
|
|
90
|
+
with pytest.raises(SalesforceDXException, match="Failed to parse the output"):
|
|
91
|
+
task()
|
|
92
|
+
|
|
93
|
+
def test_logging_stdout_and_stderr(self, mock_sfdx):
|
|
94
|
+
mock_command = create_mock_sarge_command(
|
|
95
|
+
stdout="Success: Query completed\nFound 5 records",
|
|
96
|
+
stderr="Warning: Some non-critical issue",
|
|
97
|
+
)
|
|
98
|
+
mock_sfdx.return_value = mock_command
|
|
99
|
+
|
|
100
|
+
task = create_task(SfDataCommands, {})
|
|
101
|
+
with mock.patch.object(task, "logger") as mock_logger:
|
|
102
|
+
task()
|
|
103
|
+
|
|
104
|
+
# Check that stdout lines were logged as info
|
|
105
|
+
mock_logger.info.assert_any_call("Success: Query completed")
|
|
106
|
+
mock_logger.info.assert_any_call("Found 5 records")
|
|
107
|
+
|
|
108
|
+
# Check that stderr lines were logged as error
|
|
109
|
+
mock_logger.error.assert_called_with("Warning: Some non-critical issue")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@mock.patch("cumulusci.tasks.salesforce.SfDataCommands.sfdx")
|
|
113
|
+
class TestSfDataToolingAPISupportedCommands:
|
|
114
|
+
def test_inherits_from_base(self, mock_sfdx):
|
|
115
|
+
mock_sfdx.return_value = create_mock_sarge_command()
|
|
116
|
+
task = create_task(SfDataToolingAPISupportedCommands, {})
|
|
117
|
+
assert isinstance(task, SfDataCommands)
|
|
118
|
+
|
|
119
|
+
def test_use_tooling_api_option(self, mock_sfdx):
|
|
120
|
+
mock_sfdx.return_value = create_mock_sarge_command()
|
|
121
|
+
task = create_task(SfDataToolingAPISupportedCommands, {"use_tooling_api": True})
|
|
122
|
+
# Test that the option is available (would be tested in subclasses that use it)
|
|
123
|
+
assert hasattr(task.parsed_options, "use_tooling_api")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@mock.patch("cumulusci.tasks.salesforce.SfDataCommands.sfdx")
|
|
127
|
+
class TestDataQueryTask:
|
|
128
|
+
def test_init_task_sets_command(self, mock_sfdx):
|
|
129
|
+
mock_sfdx.return_value = create_mock_sarge_command()
|
|
130
|
+
task = create_task(DataQueryTask, {"query": "SELECT Id FROM Account"})
|
|
131
|
+
task()
|
|
132
|
+
assert task.data_command == "data query"
|
|
133
|
+
|
|
134
|
+
def test_init_options_all_parameters(self, mock_sfdx):
|
|
135
|
+
json_response = {"status": 0, "result": {"records": []}}
|
|
136
|
+
mock_sfdx.return_value = create_mock_sarge_command(
|
|
137
|
+
stdout=json.dumps(json_response)
|
|
138
|
+
)
|
|
139
|
+
task = create_task(
|
|
140
|
+
DataQueryTask,
|
|
141
|
+
{
|
|
142
|
+
"query": "SELECT Id FROM Account",
|
|
143
|
+
"file": "/tmp/query.soql",
|
|
144
|
+
"all_rows": True,
|
|
145
|
+
"result_format": "csv",
|
|
146
|
+
"output_file": "/tmp/output.csv",
|
|
147
|
+
"api_version": "50.0",
|
|
148
|
+
"flags_dir": "/tmp/flags",
|
|
149
|
+
"json_output": True,
|
|
150
|
+
"use_tooling_api": True,
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
task()
|
|
154
|
+
|
|
155
|
+
call_args = mock_sfdx.call_args[1]["args"]
|
|
156
|
+
assert mock_sfdx.call_args[1]["log_note"] == "Running data command"
|
|
157
|
+
assert "--query" in call_args
|
|
158
|
+
assert "SELECT Id FROM Account" in call_args
|
|
159
|
+
assert "--file" in call_args
|
|
160
|
+
assert "/tmp/query.soql" in call_args
|
|
161
|
+
assert "--all-rows" in call_args
|
|
162
|
+
assert "--result-format" in call_args
|
|
163
|
+
assert "csv" in call_args
|
|
164
|
+
assert "--output-file" in call_args
|
|
165
|
+
assert "/tmp/output.csv" in call_args
|
|
166
|
+
assert "--api_version" in call_args
|
|
167
|
+
assert "50.0" in call_args
|
|
168
|
+
assert "--flags-dir " in call_args
|
|
169
|
+
assert "/tmp/flags" in call_args
|
|
170
|
+
assert "--json" in call_args
|
|
171
|
+
|
|
172
|
+
def test_minimal_options(self, mock_sfdx):
|
|
173
|
+
mock_sfdx.return_value = create_mock_sarge_command()
|
|
174
|
+
task = create_task(DataQueryTask, {"query": "SELECT Id FROM Account"})
|
|
175
|
+
task()
|
|
176
|
+
|
|
177
|
+
call_args = mock_sfdx.call_args[1]["args"]
|
|
178
|
+
assert "--query" in call_args
|
|
179
|
+
assert "SELECT Id FROM Account" in call_args
|
|
180
|
+
|
|
181
|
+
def test_json_output_logging(self, mock_sfdx):
|
|
182
|
+
json_response = {"status": 0, "result": {"records": [{"Id": "123"}]}}
|
|
183
|
+
stdout = json.dumps(json_response)
|
|
184
|
+
mock_sfdx.return_value = create_mock_sarge_command(stdout=stdout)
|
|
185
|
+
|
|
186
|
+
task = create_task(
|
|
187
|
+
DataQueryTask,
|
|
188
|
+
{"query": "SELECT Id FROM Account", "json_output": True},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
with mock.patch.object(task, "logger") as mock_logger:
|
|
192
|
+
task()
|
|
193
|
+
mock_logger.info.assert_called_with(json_response)
|
|
194
|
+
|
|
195
|
+
def test_run_task_json_decode_error(self, mock_sfdx):
|
|
196
|
+
mock_sfdx.return_value = create_mock_sarge_command(stdout="this is not json")
|
|
197
|
+
task = create_task(
|
|
198
|
+
DataQueryTask,
|
|
199
|
+
{"query": "SELECT Id FROM Account", "json_output": True},
|
|
200
|
+
)
|
|
201
|
+
with pytest.raises(
|
|
202
|
+
SalesforceDXException,
|
|
203
|
+
match="Failed to parse the output of the data query command",
|
|
204
|
+
):
|
|
205
|
+
task()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@mock.patch("cumulusci.tasks.salesforce.SfDataCommands.sfdx")
|
|
209
|
+
class TestDataCreateRecordTask:
|
|
210
|
+
def test_init_task_sets_command(self, mock_sfdx):
|
|
211
|
+
mock_sfdx.return_value = create_mock_sarge_command()
|
|
212
|
+
task = create_task(
|
|
213
|
+
DataCreateRecordTask,
|
|
214
|
+
{"sobject": "Account", "values": "Name='Test Account'"},
|
|
215
|
+
)
|
|
216
|
+
task()
|
|
217
|
+
assert task.data_command == "data create record"
|
|
218
|
+
|
|
219
|
+
def test_init_options_all_parameters(self, mock_sfdx):
|
|
220
|
+
json_response = {"status": 0, "result": {"id": "001000000000001"}}
|
|
221
|
+
mock_sfdx.return_value = create_mock_sarge_command(
|
|
222
|
+
stdout=json.dumps(json_response)
|
|
223
|
+
)
|
|
224
|
+
task = create_task(
|
|
225
|
+
DataCreateRecordTask,
|
|
226
|
+
{
|
|
227
|
+
"sobject": "Account",
|
|
228
|
+
"values": "Name='Test Account' Industry='Technology'",
|
|
229
|
+
"api_version": "50.0",
|
|
230
|
+
"flags_dir": "/tmp/flags",
|
|
231
|
+
"json_output": True,
|
|
232
|
+
"use_tooling_api": True,
|
|
233
|
+
},
|
|
234
|
+
)
|
|
235
|
+
task()
|
|
236
|
+
|
|
237
|
+
call_args = mock_sfdx.call_args[1]["args"]
|
|
238
|
+
assert mock_sfdx.call_args[1]["log_note"] == "Running data command"
|
|
239
|
+
assert "--sobject" in call_args
|
|
240
|
+
assert "Account" in call_args
|
|
241
|
+
assert "--values" in call_args
|
|
242
|
+
assert "Name='Test Account' Industry='Technology'" in call_args
|
|
243
|
+
assert "--api_version" in call_args
|
|
244
|
+
assert "50.0" in call_args
|
|
245
|
+
assert "--flags-dir " in call_args
|
|
246
|
+
assert "/tmp/flags" in call_args
|
|
247
|
+
assert "--json" in call_args
|
|
248
|
+
|
|
249
|
+
def test_minimal_options(self, mock_sfdx):
|
|
250
|
+
mock_sfdx.return_value = create_mock_sarge_command()
|
|
251
|
+
task = create_task(
|
|
252
|
+
DataCreateRecordTask,
|
|
253
|
+
{"sobject": "Contact", "values": "LastName='Doe'"},
|
|
254
|
+
)
|
|
255
|
+
task()
|
|
256
|
+
|
|
257
|
+
call_args = mock_sfdx.call_args[1]["args"]
|
|
258
|
+
assert "--sobject" in call_args
|
|
259
|
+
assert "Contact" in call_args
|
|
260
|
+
assert "--values" in call_args
|
|
261
|
+
assert "LastName='Doe'" in call_args
|
|
262
|
+
|
|
263
|
+
def test_missing_required_options(self, mock_sfdx):
|
|
264
|
+
# Test that required field validation works as expected
|
|
265
|
+
mock_sfdx.return_value = create_mock_sarge_command()
|
|
266
|
+
from cumulusci.core.exceptions import TaskOptionsError
|
|
267
|
+
|
|
268
|
+
with pytest.raises(TaskOptionsError, match="field required"):
|
|
269
|
+
create_task(DataCreateRecordTask, {})
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@mock.patch("cumulusci.tasks.salesforce.SfDataCommands.sfdx")
|
|
273
|
+
class TestDataDeleteRecordTask:
|
|
274
|
+
def test_init_task_sets_command(self, mock_sfdx):
|
|
275
|
+
mock_sfdx.return_value = create_mock_sarge_command()
|
|
276
|
+
task = create_task(
|
|
277
|
+
DataDeleteRecordTask, {"sobject": "Account", "record_id": "001000000000001"}
|
|
278
|
+
)
|
|
279
|
+
task()
|
|
280
|
+
assert task.data_command == "data delete record"
|
|
281
|
+
|
|
282
|
+
def test_delete_by_record_id(self, mock_sfdx):
|
|
283
|
+
json_response = {"status": 0, "result": {}}
|
|
284
|
+
mock_sfdx.return_value = create_mock_sarge_command(
|
|
285
|
+
stdout=json.dumps(json_response)
|
|
286
|
+
)
|
|
287
|
+
task = create_task(
|
|
288
|
+
DataDeleteRecordTask,
|
|
289
|
+
{
|
|
290
|
+
"sobject": "Account",
|
|
291
|
+
"record_id": "001000000000001AAA",
|
|
292
|
+
"api_version": "50.0",
|
|
293
|
+
"flags_dir": "/tmp/flags",
|
|
294
|
+
"json_output": True,
|
|
295
|
+
},
|
|
296
|
+
)
|
|
297
|
+
task()
|
|
298
|
+
|
|
299
|
+
call_args = mock_sfdx.call_args[1]["args"]
|
|
300
|
+
assert mock_sfdx.call_args[1]["log_note"] == "Running data command"
|
|
301
|
+
assert "--sobject" in call_args
|
|
302
|
+
assert "Account" in call_args
|
|
303
|
+
assert "--record-id" in call_args
|
|
304
|
+
assert "001000000000001AAA" in call_args
|
|
305
|
+
assert "--api_version" in call_args
|
|
306
|
+
assert "50.0" in call_args
|
|
307
|
+
assert "--flags-dir " in call_args
|
|
308
|
+
assert "/tmp/flags" in call_args
|
|
309
|
+
assert "--json" in call_args
|
|
310
|
+
|
|
311
|
+
def test_delete_by_where_clause(self, mock_sfdx):
|
|
312
|
+
mock_sfdx.return_value = create_mock_sarge_command()
|
|
313
|
+
task = create_task(
|
|
314
|
+
DataDeleteRecordTask,
|
|
315
|
+
{
|
|
316
|
+
"sobject": "Account",
|
|
317
|
+
"where": "Name='Test Account' AND Industry='Technology'",
|
|
318
|
+
"use_tooling_api": True,
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
task()
|
|
322
|
+
|
|
323
|
+
call_args = mock_sfdx.call_args[1]["args"]
|
|
324
|
+
# Check that sfdx was called with the right log_note
|
|
325
|
+
assert mock_sfdx.call_args[1]["log_note"] == "Running data command"
|
|
326
|
+
assert "--sobject" in call_args
|
|
327
|
+
assert "Account" in call_args
|
|
328
|
+
assert "--where" in call_args
|
|
329
|
+
assert "Name='Test Account' AND Industry='Technology'" in call_args
|
|
330
|
+
|
|
331
|
+
def test_minimal_options(self, mock_sfdx):
|
|
332
|
+
mock_sfdx.return_value = create_mock_sarge_command()
|
|
333
|
+
task = create_task(
|
|
334
|
+
DataDeleteRecordTask,
|
|
335
|
+
{"sobject": "Contact"},
|
|
336
|
+
)
|
|
337
|
+
task()
|
|
338
|
+
|
|
339
|
+
call_args = mock_sfdx.call_args[1]["args"]
|
|
340
|
+
assert "--sobject" in call_args
|
|
341
|
+
assert "Contact" in call_args
|
|
342
|
+
# Should not have --record-id or --where if not provided
|
|
343
|
+
assert "--record-id" not in call_args
|
|
344
|
+
assert "--where" not in call_args
|
|
345
|
+
|
|
346
|
+
def test_both_record_id_and_where_provided(self, mock_sfdx):
|
|
347
|
+
# Test that both options can be provided (though this might not be practical)
|
|
348
|
+
mock_sfdx.return_value = create_mock_sarge_command()
|
|
349
|
+
task = create_task(
|
|
350
|
+
DataDeleteRecordTask,
|
|
351
|
+
{
|
|
352
|
+
"sobject": "Account",
|
|
353
|
+
"record_id": "001000000000001",
|
|
354
|
+
"where": "Name='Test Account'",
|
|
355
|
+
},
|
|
356
|
+
)
|
|
357
|
+
task()
|
|
358
|
+
|
|
359
|
+
call_args = mock_sfdx.call_args[1]["args"]
|
|
360
|
+
assert "--record-id" in call_args
|
|
361
|
+
assert "001000000000001" in call_args
|
|
362
|
+
assert "--where" in call_args
|
|
363
|
+
assert "Name='Test Account'" in call_args
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from cumulusci.core.tasks import BaseTask
|
|
4
|
+
from cumulusci.utils.options import CCIOptions, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GetFirstItemFromRecordListTask(BaseTask):
|
|
8
|
+
class Options(CCIOptions):
|
|
9
|
+
result: dict[str, Any] = Field(
|
|
10
|
+
None, description="The result from SF CLI operations."
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
parsed_options: Options
|
|
14
|
+
|
|
15
|
+
def _run_task(self):
|
|
16
|
+
self.return_values = self.parsed_options.result["records"][0]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import logging
|
|
2
3
|
import os
|
|
3
4
|
from datetime import date
|
|
4
5
|
from pathlib import Path
|
|
@@ -23,16 +24,12 @@ class EnvManagementOption(CCIOptions):
|
|
|
23
24
|
)
|
|
24
25
|
datatype: str = Field(
|
|
25
26
|
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,
|
|
27
|
+
description="The datatype of the environment variable. Defaults to string. Valid values are string, bool, int, float, date, list, dict, path, directory, filename, vcs_repo",
|
|
27
28
|
)
|
|
28
29
|
set: bool = Field(
|
|
29
30
|
default=False,
|
|
30
31
|
description="If True, sets the value of the environment variable if it is not already set. Defaults to False",
|
|
31
32
|
)
|
|
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
33
|
|
|
37
34
|
@validator("datatype")
|
|
38
35
|
def validate_datatype(cls, v):
|
|
@@ -47,62 +44,77 @@ class EnvManagementOption(CCIOptions):
|
|
|
47
44
|
"path",
|
|
48
45
|
"directory",
|
|
49
46
|
"filename",
|
|
50
|
-
"
|
|
47
|
+
"vcs_repo",
|
|
51
48
|
]:
|
|
52
49
|
raise ValueError(f"Invalid datatype: {v}")
|
|
53
50
|
return v
|
|
54
51
|
|
|
55
52
|
def formated_value(
|
|
56
53
|
self,
|
|
54
|
+
task_values: dict[str, Any],
|
|
57
55
|
project_config: Optional[BaseProjectConfig],
|
|
58
56
|
org_config: Optional[OrgConfig],
|
|
59
|
-
|
|
57
|
+
logger: logging.Logger,
|
|
58
|
+
) -> None:
|
|
60
59
|
value = os.getenv(self.name, self.default)
|
|
61
60
|
datatype = self.datatype or "string"
|
|
62
61
|
|
|
63
62
|
try:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
63
|
+
if self.name not in task_values:
|
|
64
|
+
match datatype:
|
|
65
|
+
case "string":
|
|
66
|
+
task_values[self.name] = str(value)
|
|
67
|
+
case "bool":
|
|
68
|
+
v = DummyValidatorModel(b=value).b
|
|
69
|
+
task_values[self.name] = v
|
|
70
|
+
case "int":
|
|
71
|
+
v = DummyValidatorModel(i=value).i
|
|
72
|
+
task_values[self.name] = v
|
|
73
|
+
case "float":
|
|
74
|
+
v = DummyValidatorModel(f=value).f
|
|
75
|
+
task_values[self.name] = v
|
|
76
|
+
case "date":
|
|
77
|
+
v = DummyValidatorModel(d=date.fromisoformat(str(value))).d
|
|
78
|
+
task_values[self.name] = v
|
|
79
|
+
case "list":
|
|
80
|
+
v = value if isinstance(value, list) else value.split(",")
|
|
81
|
+
task_values[self.name] = v
|
|
82
|
+
case "dict":
|
|
83
|
+
v = value if isinstance(value, dict) else json.loads(str(value))
|
|
84
|
+
task_values[self.name] = v
|
|
85
|
+
case "path":
|
|
86
|
+
v = Path(str(value))
|
|
87
|
+
task_values[self.name] = v.absolute()
|
|
88
|
+
case "directory":
|
|
89
|
+
v = Path(str(value)).parent.absolute()
|
|
90
|
+
task_values[self.name] = v.absolute()
|
|
91
|
+
case "filename":
|
|
92
|
+
v = Path(str(value)).name
|
|
93
|
+
task_values[self.name] = v
|
|
94
|
+
case "vcs_repo":
|
|
95
|
+
task_config = TaskConfig(
|
|
96
|
+
{"options": {"url": self.default, "name": self.name}}
|
|
97
|
+
)
|
|
98
|
+
task = VcsRemoteBranch(project_config, task_config, org_config)
|
|
99
|
+
result = task()
|
|
100
|
+
task_values[self.name] = result["url"]
|
|
101
|
+
task_values[f"{self.name}_BRANCH"] = result["branch"]
|
|
102
|
+
case _:
|
|
103
|
+
raise ValueError(f"Invalid datatype: {datatype}")
|
|
104
|
+
else:
|
|
105
|
+
logger.info(f"Variable {self.name} already set. Skipping.")
|
|
106
|
+
|
|
101
107
|
except Exception as e:
|
|
102
108
|
raise ValueError(
|
|
103
109
|
f"Formatting Error: {value} for datatype: {datatype} - {e}"
|
|
104
110
|
)
|
|
105
111
|
|
|
112
|
+
if self.set:
|
|
113
|
+
os.environ[self.name] = str(task_values[self.name])
|
|
114
|
+
|
|
115
|
+
if self.set and self.datatype == "vcs_repo":
|
|
116
|
+
os.environ[f"{self.name}_BRANCH"] = str(task_values[f"{self.name}_BRANCH"])
|
|
117
|
+
|
|
106
118
|
|
|
107
119
|
class DummyValidatorModel(BaseModel):
|
|
108
120
|
b: Optional[bool]
|
|
@@ -124,11 +136,9 @@ class EnvManagement(BaseTask):
|
|
|
124
136
|
self.return_values = {}
|
|
125
137
|
|
|
126
138
|
for env_option in self.parsed_options.envs:
|
|
127
|
-
|
|
128
|
-
self.project_config, self.org_config
|
|
139
|
+
env_option.formated_value(
|
|
140
|
+
self.return_values, self.project_config, self.org_config, self.logger
|
|
129
141
|
)
|
|
130
|
-
if env_option.set and env_option.name not in os.environ:
|
|
131
|
-
os.environ[env_option.name] = str_value
|
|
132
142
|
|
|
133
143
|
return self.return_values
|
|
134
144
|
|
|
@@ -139,19 +149,38 @@ class VcsRemoteBranch(BaseTask):
|
|
|
139
149
|
...,
|
|
140
150
|
description="Gets if the remote branch name exist with the same name in the remote repository.",
|
|
141
151
|
)
|
|
152
|
+
name: str = Field(
|
|
153
|
+
...,
|
|
154
|
+
description="The name of the environment variable.",
|
|
155
|
+
)
|
|
142
156
|
|
|
143
157
|
parsed_options: Options
|
|
144
158
|
|
|
145
159
|
def _run_task(self):
|
|
146
160
|
self.return_values = {}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
161
|
+
|
|
162
|
+
# Get current branch name. Based on Local Git Branch if not available from Environment Variable
|
|
163
|
+
local_branch = os.getenv(
|
|
164
|
+
f"{self.parsed_options.name}_BRANCH", self.project_config.repo_branch
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Get repository URL from Environment Variable
|
|
168
|
+
self.return_values["url"] = os.getenv(
|
|
169
|
+
self.parsed_options.name, self.parsed_options.url
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
repo = get_repo_from_url(self.project_config, self.return_values["url"])
|
|
150
173
|
|
|
151
174
|
try:
|
|
152
175
|
branch = repo.branch(local_branch)
|
|
153
|
-
self.
|
|
176
|
+
self.logger.info(
|
|
177
|
+
f"Branch {local_branch} found in repository {self.return_values['url']}."
|
|
178
|
+
)
|
|
179
|
+
self.return_values["branch"] = branch.name
|
|
154
180
|
except Exception:
|
|
155
|
-
self.
|
|
181
|
+
self.logger.warning(
|
|
182
|
+
f"Branch {local_branch} not found in repository {self.return_values['url']}. Using default branch {repo.default_branch}"
|
|
183
|
+
)
|
|
184
|
+
self.return_values["branch"] = repo.default_branch
|
|
156
185
|
|
|
157
186
|
return self.return_values
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from cumulusci.core.config import (
|
|
4
|
+
BaseProjectConfig,
|
|
5
|
+
OrgConfig,
|
|
6
|
+
TaskConfig,
|
|
7
|
+
UniversalConfig,
|
|
8
|
+
)
|
|
9
|
+
from cumulusci.tasks.utility.data_management import GetFirstItemFromRecordListTask
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def a_task():
|
|
14
|
+
universal_config = UniversalConfig()
|
|
15
|
+
project_config = BaseProjectConfig(universal_config, config={"noyaml": True})
|
|
16
|
+
org_config = OrgConfig({}, "test")
|
|
17
|
+
|
|
18
|
+
def _a_task(options):
|
|
19
|
+
task_config = TaskConfig({"options": options})
|
|
20
|
+
return GetFirstItemFromRecordListTask(project_config, task_config, org_config)
|
|
21
|
+
|
|
22
|
+
return _a_task
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestGetFirstItemFromRecordListTask:
|
|
26
|
+
def test_returns_first_item(self, a_task):
|
|
27
|
+
"""Tests that the task returns the first item from the 'records' list."""
|
|
28
|
+
result_input = {
|
|
29
|
+
"records": [
|
|
30
|
+
{"Id": "001", "Name": "Test Account"},
|
|
31
|
+
{"Id": "002", "Name": "Another Account"},
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
task = a_task({"result": result_input})
|
|
35
|
+
task()
|
|
36
|
+
assert task.return_values == {"Id": "001", "Name": "Test Account"}
|
|
37
|
+
|
|
38
|
+
def test_empty_record_list(self, a_task):
|
|
39
|
+
"""Tests that an IndexError is raised for an empty 'records' list."""
|
|
40
|
+
result_input = {"records": []}
|
|
41
|
+
task = a_task({"result": result_input})
|
|
42
|
+
with pytest.raises(IndexError):
|
|
43
|
+
task()
|
|
@@ -18,7 +18,12 @@ class TestVcsRemoteBranch(unittest.TestCase):
|
|
|
18
18
|
project_config = create_project_config()
|
|
19
19
|
project_config.repo_info["branch"] = "feature/branch-1"
|
|
20
20
|
task_config = TaskConfig(
|
|
21
|
-
{
|
|
21
|
+
{
|
|
22
|
+
"options": {
|
|
23
|
+
"url": "https://github.com/TestOwner/TestRepo",
|
|
24
|
+
"name": "VCS_URL",
|
|
25
|
+
}
|
|
26
|
+
}
|
|
22
27
|
)
|
|
23
28
|
|
|
24
29
|
with patch(
|
|
@@ -32,14 +37,22 @@ class TestVcsRemoteBranch(unittest.TestCase):
|
|
|
32
37
|
|
|
33
38
|
task = VcsRemoteBranch(project_config, task_config)
|
|
34
39
|
task()
|
|
35
|
-
self.assertEqual(
|
|
40
|
+
self.assertEqual(
|
|
41
|
+
task.return_values["url"], "https://github.com/TestOwner/TestRepo"
|
|
42
|
+
)
|
|
43
|
+
self.assertEqual(task.return_values["branch"], "feature/branch-1")
|
|
36
44
|
repo_mock.branch.assert_called_once_with("feature/branch-1")
|
|
37
45
|
|
|
38
46
|
def test_run_task_branch_not_exist(self):
|
|
39
47
|
project_config = create_project_config()
|
|
40
48
|
project_config.repo_info["branch"] = "feature/branch-1"
|
|
41
49
|
task_config = TaskConfig(
|
|
42
|
-
{
|
|
50
|
+
{
|
|
51
|
+
"options": {
|
|
52
|
+
"url": "https://github.com/TestOwner/TestRepo",
|
|
53
|
+
"name": "VCS_URL",
|
|
54
|
+
}
|
|
55
|
+
}
|
|
43
56
|
)
|
|
44
57
|
|
|
45
58
|
with patch(
|
|
@@ -52,7 +65,10 @@ class TestVcsRemoteBranch(unittest.TestCase):
|
|
|
52
65
|
|
|
53
66
|
task = VcsRemoteBranch(project_config, task_config)
|
|
54
67
|
task()
|
|
55
|
-
self.assertEqual(
|
|
68
|
+
self.assertEqual(
|
|
69
|
+
task.return_values["url"], "https://github.com/TestOwner/TestRepo"
|
|
70
|
+
)
|
|
71
|
+
self.assertEqual(task.return_values["branch"], "main")
|
|
56
72
|
repo_mock.branch.assert_called_once_with("feature/branch-1")
|
|
57
73
|
|
|
58
74
|
|
|
@@ -177,20 +193,21 @@ class TestEnvManagement(unittest.TestCase):
|
|
|
177
193
|
task()
|
|
178
194
|
|
|
179
195
|
@patch("cumulusci.tasks.utility.env_management.VcsRemoteBranch")
|
|
180
|
-
def
|
|
181
|
-
os.environ["TEST_VAR"] = "" # vcs_branch doesn't use env var
|
|
182
|
-
|
|
196
|
+
def test_vcs_repo_datatype(self, vcs_mock):
|
|
183
197
|
vcs_instance_mock = vcs_mock.return_value
|
|
184
|
-
vcs_instance_mock.return_value = {
|
|
198
|
+
vcs_instance_mock.return_value = {
|
|
199
|
+
"url": "https://github.com/TestOwner/TestRepo",
|
|
200
|
+
"branch": "my-feature-branch",
|
|
201
|
+
}
|
|
185
202
|
|
|
186
203
|
task_config = TaskConfig(
|
|
187
204
|
{
|
|
188
205
|
"options": {
|
|
189
206
|
"envs": [
|
|
190
207
|
{
|
|
191
|
-
"name": "
|
|
192
|
-
"datatype": "
|
|
193
|
-
"
|
|
208
|
+
"name": "VCS_URL",
|
|
209
|
+
"datatype": "vcs_repo",
|
|
210
|
+
"default": "https://github.com/TestOwner/TestRepo",
|
|
194
211
|
}
|
|
195
212
|
]
|
|
196
213
|
}
|
|
@@ -199,7 +216,8 @@ class TestEnvManagement(unittest.TestCase):
|
|
|
199
216
|
task = EnvManagement(self.project_config, task_config, self.org_config)
|
|
200
217
|
result = task()
|
|
201
218
|
|
|
202
|
-
self.assertEqual(result["
|
|
219
|
+
self.assertEqual(result["VCS_URL"], "https://github.com/TestOwner/TestRepo")
|
|
220
|
+
self.assertEqual(result["VCS_URL_BRANCH"], "my-feature-branch")
|
|
203
221
|
vcs_mock.assert_called_once()
|
|
204
222
|
|
|
205
223
|
@patch.dict(os.environ, {}, clear=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cumulusci-plus
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.18
|
|
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.18 (2025-08-23)
|
|
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
|
-
- Add
|
|
138
|
+
- Add sf data command of create, query and delete. by [@rupeshjSFDC](https://github.com/rupeshjSFDC) in [#61](https://github.com/jorgesolebur/CumulusCI/pull/61)
|
|
139
139
|
|
|
140
|
-
**Full Changelog**: https://github.com/jorgesolebur/CumulusCI/compare/v5.0.
|
|
140
|
+
**Full Changelog**: https://github.com/jorgesolebur/CumulusCI/compare/v5.0.17...v5.0.18
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
cumulusci/__about__.py,sha256=
|
|
1
|
+
cumulusci/__about__.py,sha256=rNEejWvxeGfAnWPU00R1oUZaWa42-NuQA6ta50JVBY0,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=RQN4mskRwSYaP33g-b9SZg8j62rU5ynpCE7ZBxfDqFY,73476
|
|
6
6
|
cumulusci/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
cumulusci/cli/cci.py,sha256=yAq8jFoGde6g_1TeAAjzZYsk77itiONCQGBFe3g3nOs,11836
|
|
8
8
|
cumulusci/cli/error.py,sha256=znj0YN8D2Grozm1u7mZAsJlmmdGebbuy0c1ofQluL4Q,4410
|
|
@@ -516,6 +516,7 @@ cumulusci/tasks/salesforce/RetrievePackaged.py,sha256=XMdA3qBIuGJaZnPNeZBKupz9N6
|
|
|
516
516
|
cumulusci/tasks/salesforce/RetrieveReportsAndDashboards.py,sha256=-pVD0RJ4vsB1TM9b7VcPQAV-pO4LTAm-YoESq4bM2dA,3289
|
|
517
517
|
cumulusci/tasks/salesforce/RetrieveUnpackaged.py,sha256=_9jglbNWmjg9kq5EkvCusprrt14czurBtNM2hbU9RFI,1174
|
|
518
518
|
cumulusci/tasks/salesforce/SOQLQuery.py,sha256=RFV-bLzvAx_m4SWWLMyCKuA3ie08iLwLyrOVyhpNCPE,1730
|
|
519
|
+
cumulusci/tasks/salesforce/SfDataCommands.py,sha256=tCrjvxZDk4vrVmeJo7BR5uTEZUUbhbAGNoRuwt4zn1k,6094
|
|
519
520
|
cumulusci/tasks/salesforce/UninstallLocal.py,sha256=6nnRDyFrwIcXd9WygDHIMhMQYBZMFEQ75Sh6nNxivfg,492
|
|
520
521
|
cumulusci/tasks/salesforce/UninstallLocalBundles.py,sha256=eG4yoDJdjruo9kwfcQU-i5_AVt473uWF7SvLD7IQoSw,759
|
|
521
522
|
cumulusci/tasks/salesforce/UninstallLocalNamespacedBundles.py,sha256=KgM6TPSkczAdeM10fOAtijs4uszJ_f219lGgVNwJ1eM,2210
|
|
@@ -561,6 +562,7 @@ cumulusci/tasks/salesforce/tests/test_RetrievePackaged.py,sha256=jhXYXv6I54HUfOf
|
|
|
561
562
|
cumulusci/tasks/salesforce/tests/test_RetrieveReportsAndDashboards.py,sha256=6ePEXIqMdNLKcnxUkwnpWSqJ1TaWFfAcl3lEhbsqFFI,1730
|
|
562
563
|
cumulusci/tasks/salesforce/tests/test_RetrieveUnpackaged.py,sha256=mXMcw0a1til3R8eMm7KGZ5oftqKT5XLFoZ047UAO0Z8,668
|
|
563
564
|
cumulusci/tasks/salesforce/tests/test_SOQLQuery.py,sha256=YHsdSzFNEY8R5qbT0al9I0mYHWNT8P4hqYji2gDVnEU,849
|
|
565
|
+
cumulusci/tasks/salesforce/tests/test_SfDataCommands.py,sha256=JdM7P-lDXKtvxK4UlHMOIfzCVz6uH2eaIyyJEZKenuk,13353
|
|
564
566
|
cumulusci/tasks/salesforce/tests/test_UninstallLocal.py,sha256=v7UtmVPFGeH13mSnuZi8eI5vaDeO7WlDu1bhM3s9-fk,582
|
|
565
567
|
cumulusci/tasks/salesforce/tests/test_UninstallLocalBundles.py,sha256=oz8xuYGda4aYXskIAB0mUAJpgroOhV9Ahb-a4oNJtio,557
|
|
566
568
|
cumulusci/tasks/salesforce/tests/test_UninstallLocalNamespacedBundles.py,sha256=oj_OpxDnTRSX782aWSGsttnybrO3cZh3Mtk_NiLZiOs,907
|
|
@@ -611,8 +613,10 @@ cumulusci/tasks/tests/test_pushfails.py,sha256=9JG9D0iD4dR-1fKheaRN7BEy3lzzuOKeR
|
|
|
611
613
|
cumulusci/tasks/tests/test_salesforce.py,sha256=yCGtuHapxyAEmXQhuF2g2fh2naknTu7Md4OfEJQvGAA,2594
|
|
612
614
|
cumulusci/tasks/tests/test_sfdx.py,sha256=oUbHo28d796m5RuskXMLitJw2rCLjjXIfxggzr4gsso,3545
|
|
613
615
|
cumulusci/tasks/tests/test_util.py,sha256=D1T0QnvPTS0PHeZWo2xiVgE1jVTYcLzGTGHwEIoVmxk,7296
|
|
614
|
-
cumulusci/tasks/utility/
|
|
615
|
-
cumulusci/tasks/utility/
|
|
616
|
+
cumulusci/tasks/utility/data_management.py,sha256=7DbK0uxMxj_e4WuH08gWFVxr_ZBMrvHvumHnM_oi2YM,443
|
|
617
|
+
cumulusci/tasks/utility/env_management.py,sha256=hJX6ySEiXD2oFW38JqbGQKMj89ucxdSBsPwytSdkgO8,6591
|
|
618
|
+
cumulusci/tasks/utility/tests/test_data_management.py,sha256=f3F1yFikaNEkzllMXZa67gNX0T_PCC-4zEoL8kUII_Q,1363
|
|
619
|
+
cumulusci/tasks/utility/tests/test_env_management.py,sha256=fw34meWGOe1YYZO449MMCi2O7BgSaOA_I_wScrIr1Uk,8702
|
|
616
620
|
cumulusci/tasks/vcs/__init__.py,sha256=ZzpMZnhooXZ6r_ywBVTS3UNw9uMcXW6h33LylRqTDK0,700
|
|
617
621
|
cumulusci/tasks/vcs/commit_status.py,sha256=hgPUVHeQyIfMsCuwrw2RI-ufnbjdRARc6HI3BEgdcxI,2332
|
|
618
622
|
cumulusci/tasks/vcs/create_commit_status.py,sha256=kr_OFM3J32jxusS1og-K91Kl1F_Nr7SlevRsJKBhifs,1565
|
|
@@ -738,9 +742,9 @@ cumulusci/vcs/tests/dummy_service.py,sha256=RltOUpMIhSDNrfxk0LhLqlH4ppC0sK6NC2cO
|
|
|
738
742
|
cumulusci/vcs/tests/test_vcs_base.py,sha256=9mp6uZ3lTxY4onjUNCucp9N9aB3UylKS7_2Zu_hdAZw,24331
|
|
739
743
|
cumulusci/vcs/tests/test_vcs_bootstrap.py,sha256=N0NA48-rGNIIjY3Z7PtVnNwHObSlEGDk2K55TQGI8g4,27954
|
|
740
744
|
cumulusci/vcs/utils/__init__.py,sha256=py4fEcHM7Vd0M0XWznOlywxaeCtG3nEVGmELmEKVGU8,869
|
|
741
|
-
cumulusci_plus-5.0.
|
|
742
|
-
cumulusci_plus-5.0.
|
|
743
|
-
cumulusci_plus-5.0.
|
|
744
|
-
cumulusci_plus-5.0.
|
|
745
|
-
cumulusci_plus-5.0.
|
|
746
|
-
cumulusci_plus-5.0.
|
|
745
|
+
cumulusci_plus-5.0.18.dist-info/METADATA,sha256=EjWQpoxW3IMvVViclvXJR9WyKkDlsnxTxZrsX6m0Zwk,5769
|
|
746
|
+
cumulusci_plus-5.0.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
747
|
+
cumulusci_plus-5.0.18.dist-info/entry_points.txt,sha256=nTtu04b9iLXhzADcTrb5PwmdXE6e2MTUAMh9OK6Z2pg,80
|
|
748
|
+
cumulusci_plus-5.0.18.dist-info/licenses/AUTHORS.rst,sha256=PvewjKImdKPhhJ6xR2EEZ4T7GbpY2ZeAeyWm2aLtiMQ,676
|
|
749
|
+
cumulusci_plus-5.0.18.dist-info/licenses/LICENSE,sha256=NFsF_s7RVXk2dU6tmRAN8wF45pnD98VZ5IwqOsyBcaU,1499
|
|
750
|
+
cumulusci_plus-5.0.18.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|