cumulusci-plus 5.0.17__py3-none-any.whl → 5.0.19__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.
@@ -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
@@ -95,6 +95,15 @@ def test_run_task_success():
95
95
  json={
96
96
  "done": True,
97
97
  "totalSize": 1,
98
+ "records": [],
99
+ },
100
+ )
101
+ responses.add(
102
+ responses.GET,
103
+ query_url,
104
+ json={
105
+ "done": True,
106
+ "totalSize": 0,
98
107
  "records": [
99
108
  {
100
109
  "attributes": {
@@ -115,9 +124,12 @@ def test_run_task_success():
115
124
  result = task._run_task()
116
125
  assert result == "001R0000029IyDPIA0"
117
126
  assert responses.calls[0].request.params == {
127
+ "q": "SELECT Id, Name FROM Profile WHERE FullName = 'Test Profile Name' LIMIT 1"
128
+ }
129
+ assert responses.calls[1].request.params == {
118
130
  "q": "SELECT Id, Name FROM UserLicense WHERE Name = 'Foo' LIMIT 1"
119
131
  }
120
- soap_body = responses.calls[1].request.body
132
+ soap_body = responses.calls[2].request.body
121
133
  assert "<Name>Test Profile Name</Name>" in str(soap_body)
122
134
  assert "<UserLicenseId>10056000000VGjUAAW</UserLicenseId>" in str(soap_body)
123
135
  assert "<Description>Have fun stormin da castle</Description>" in str(soap_body)
@@ -135,6 +147,15 @@ def test_run_task_fault():
135
147
  )
136
148
  task.org_config._latest_api_version = "53.0"
137
149
 
150
+ responses.add(
151
+ responses.GET,
152
+ "https://test.salesforce.com/services/data/v53.0/query/",
153
+ json={
154
+ "done": True,
155
+ "totalSize": 0,
156
+ "records": [],
157
+ },
158
+ )
138
159
  responses.add(
139
160
  responses.POST,
140
161
  "https://test.salesforce.com/services/Soap/u/53.0/ORG_ID",
@@ -157,6 +178,17 @@ def test_run_task_field_error():
157
178
  },
158
179
  )
159
180
  task.org_config._latest_api_version = "53.0"
181
+
182
+ responses.add(
183
+ responses.GET,
184
+ "https://test.salesforce.com/services/data/v53.0/query/",
185
+ json={
186
+ "done": True,
187
+ "totalSize": 0,
188
+ "records": [],
189
+ },
190
+ )
191
+
160
192
  responses.add(
161
193
  responses.POST,
162
194
  "https://test.salesforce.com/services/Soap/u/53.0/ORG_ID",
@@ -180,6 +212,16 @@ def test_run_task_error():
180
212
  )
181
213
  task.org_config._latest_api_version = "53.0"
182
214
 
215
+ responses.add(
216
+ responses.GET,
217
+ "https://test.salesforce.com/services/data/v53.0/query/",
218
+ json={
219
+ "done": True,
220
+ "totalSize": 0,
221
+ "records": [],
222
+ },
223
+ )
224
+
183
225
  responses.add(
184
226
  responses.POST,
185
227
  "https://test.salesforce.com/services/Soap/u/53.0/ORG_ID",
@@ -200,3 +242,85 @@ def test_task_options_error():
200
242
  "description": "Foo",
201
243
  },
202
244
  )
245
+
246
+
247
+ @responses.activate
248
+ def test_run_task_success_with_collision_check():
249
+ query_url = "https://test.salesforce.com/services/data/v53.0/query/"
250
+
251
+ task = create_task(
252
+ CreateBlankProfile,
253
+ {
254
+ "license": "Foo",
255
+ "name": "Test Profile Name",
256
+ "description": "Have fun stormin da castle",
257
+ },
258
+ )
259
+ task.org_config._latest_api_version = "53.0"
260
+
261
+ responses.add(
262
+ responses.GET,
263
+ query_url,
264
+ json={
265
+ "done": True,
266
+ "totalSize": 1,
267
+ "records": [
268
+ {
269
+ "attributes": {
270
+ "type": "Profile",
271
+ "url": "/services/data/v53.0/sobjects/Profile/10056000000VGjUAAW",
272
+ },
273
+ "Id": "10056000000VGjUAAW",
274
+ "Name": "Test Profile Name",
275
+ }
276
+ ],
277
+ },
278
+ )
279
+
280
+ result = task._run_task()
281
+ assert result == "10056000000VGjUAAW"
282
+ assert responses.calls[0].request.params == {
283
+ "q": "SELECT Id, Name FROM Profile WHERE FullName = 'Test Profile Name' LIMIT 1"
284
+ }
285
+
286
+
287
+ @responses.activate
288
+ def test_run_task_with_invalid_license():
289
+ query_url = "https://test.salesforce.com/services/data/v53.0/query/"
290
+
291
+ task = create_task(
292
+ CreateBlankProfile,
293
+ {
294
+ "license": "Foo",
295
+ "name": "Test Profile Name",
296
+ "description": "Have fun stormin da castle",
297
+ },
298
+ )
299
+ task.org_config._latest_api_version = "53.0"
300
+
301
+ responses.add(
302
+ responses.GET,
303
+ query_url,
304
+ json={
305
+ "done": True,
306
+ "totalSize": 1,
307
+ "records": [],
308
+ },
309
+ )
310
+ responses.add(
311
+ responses.GET,
312
+ query_url,
313
+ json={
314
+ "done": True,
315
+ "totalSize": 0,
316
+ "records": [],
317
+ },
318
+ )
319
+ responses.add(
320
+ responses.POST,
321
+ "https://test.salesforce.com/services/Soap/u/53.0/ORG_ID",
322
+ RESPONSE_SUCCESS,
323
+ )
324
+ with pytest.raises(TaskOptionsError) as e:
325
+ task._run_task()
326
+ assert "License name 'Foo' was not found." in str(e)
@@ -362,7 +362,7 @@ def test_install_dependency_installs_unmanaged():
362
362
 
363
363
  task._install_dependency(task.dependencies[0])
364
364
  task.dependencies[0].install.assert_called_once_with(
365
- task.project_config, task.org_config
365
+ task.project_config, task.org_config, task.options
366
366
  )
367
367
 
368
368
 
@@ -64,6 +64,9 @@ class UpdateDependencies(BaseSalesforceTask):
64
64
  "base_package_url_format": {
65
65
  "description": "If `interactive` is set to True, display package Ids using a format string ({} will be replaced with the package Id)."
66
66
  },
67
+ "force_pre_post_install": {
68
+ "description": "Forces the pre-install and post-install steps to be run. Defaults to False."
69
+ },
67
70
  **{k: v for k, v in PACKAGE_INSTALL_TASK_OPTIONS.items() if k != "password"},
68
71
  }
69
72
 
@@ -220,8 +223,11 @@ class UpdateDependencies(BaseSalesforceTask):
220
223
  if not click.confirm("Continue to install dependencies?", default=True):
221
224
  raise CumulusCIException("Dependency installation was canceled.")
222
225
 
223
- for d in dependencies:
224
- self._install_dependency(d)
226
+ if dependencies:
227
+ self.logger.info("Installing dependencies:")
228
+
229
+ for d in dependencies:
230
+ self._install_dependency(d)
225
231
 
226
232
  self.org_config.reset_installed_packages()
227
233
 
@@ -233,7 +239,7 @@ class UpdateDependencies(BaseSalesforceTask):
233
239
  self.project_config, self.org_config, self.install_options
234
240
  )
235
241
  else:
236
- dependency.install(self.project_config, self.org_config)
242
+ dependency.install(self.project_config, self.org_config, self.options)
237
243
 
238
244
  def freeze(self, step):
239
245
  if self.options["interactive"]:
@@ -109,14 +109,10 @@ class EnvManagementOption(CCIOptions):
109
109
  f"Formatting Error: {value} for datatype: {datatype} - {e}"
110
110
  )
111
111
 
112
- if self.set and self.name not in os.environ:
112
+ if self.set:
113
113
  os.environ[self.name] = str(task_values[self.name])
114
114
 
115
- if (
116
- self.set
117
- and self.datatype == "vcs_repo"
118
- and f"{self.name}_BRANCH" not in os.environ
119
- ):
115
+ if self.set and self.datatype == "vcs_repo":
120
116
  os.environ[f"{self.name}_BRANCH"] = str(task_values[f"{self.name}_BRANCH"])
121
117
 
122
118
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cumulusci-plus
3
- Version: 5.0.17
3
+ Version: 5.0.19
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.17 (2025-08-21)
130
+ ## v5.0.19 (2025-09-04)
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 custom data type vcs_repo. by [@rupeshjSFDC](https://github.com/rupeshjSFDC) in [#59](https://github.com/jorgesolebur/CumulusCI/pull/59)
138
+ - Feature/pm 2055 pre post flow by [@rupeshjSFDC](https://github.com/rupeshjSFDC) in [#63](https://github.com/jorgesolebur/CumulusCI/pull/63)
139
139
 
140
- **Full Changelog**: https://github.com/jorgesolebur/CumulusCI/compare/v5.0.16...v5.0.17
140
+ **Full Changelog**: https://github.com/jorgesolebur/CumulusCI/compare/v5.0.18...v5.0.19