cumulusci-plus 5.0.22__py3-none-any.whl → 5.0.24__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/tests/test_flow.py +279 -2
- cumulusci/core/flowrunner.py +88 -6
- cumulusci/cumulusci.yml +12 -0
- cumulusci/tasks/create_package_version.py +37 -5
- cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
- cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
- cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
- cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
- cumulusci/tasks/tests/test_util.py +2 -1
- cumulusci/utils/__init__.py +1 -1
- {cumulusci_plus-5.0.22.dist-info → cumulusci_plus-5.0.24.dist-info}/METADATA +7 -6
- {cumulusci_plus-5.0.22.dist-info → cumulusci_plus-5.0.24.dist-info}/RECORD +17 -13
- {cumulusci_plus-5.0.22.dist-info → cumulusci_plus-5.0.24.dist-info}/WHEEL +0 -0
- {cumulusci_plus-5.0.22.dist-info → cumulusci_plus-5.0.24.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.22.dist-info → cumulusci_plus-5.0.24.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.22.dist-info → cumulusci_plus-5.0.24.dist-info}/licenses/LICENSE +0 -0
cumulusci/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "5.0.
|
|
1
|
+
__version__ = "5.0.24"
|
cumulusci/cli/tests/test_flow.py
CHANGED
|
@@ -4,9 +4,10 @@ import click
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
6
|
from cumulusci.cli.runtime import CliRuntime
|
|
7
|
-
from cumulusci.core.config import FlowConfig
|
|
7
|
+
from cumulusci.core.config import FlowConfig, OrgConfig
|
|
8
8
|
from cumulusci.core.exceptions import CumulusCIException, FlowNotFoundError
|
|
9
|
-
from cumulusci.core.flowrunner import FlowCoordinator
|
|
9
|
+
from cumulusci.core.flowrunner import FlowCoordinator, FlowStepSpec, StepSpec
|
|
10
|
+
from cumulusci.tests.util import create_project_config
|
|
10
11
|
|
|
11
12
|
from .. import flow
|
|
12
13
|
from .utils import DummyTask, run_click_command
|
|
@@ -274,3 +275,279 @@ def test_flow_run__org_delete_error(echo):
|
|
|
274
275
|
echo.assert_any_call(
|
|
275
276
|
"Scratch org deletion failed. Ignoring the error below to complete the flow:"
|
|
276
277
|
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# Tests for new FlowStepSpec and flow skipping functionality
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class TestFlowStepSpec:
|
|
284
|
+
"""Test the FlowStepSpec class functionality."""
|
|
285
|
+
|
|
286
|
+
def test_flowstep_spec_creation(self):
|
|
287
|
+
"""Test that FlowStepSpec can be created with proper inheritance."""
|
|
288
|
+
project_config = create_project_config("TestOwner", "TestRepo")
|
|
289
|
+
|
|
290
|
+
flow_step = FlowStepSpec(
|
|
291
|
+
task_config={"test": "value"},
|
|
292
|
+
step_num="1.0",
|
|
293
|
+
task_name="test_flow",
|
|
294
|
+
task_class=None,
|
|
295
|
+
project_config=project_config,
|
|
296
|
+
allow_failure=False,
|
|
297
|
+
when="org_config.username == 'test@example.com'",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
assert isinstance(flow_step, StepSpec)
|
|
301
|
+
assert isinstance(flow_step, FlowStepSpec)
|
|
302
|
+
assert flow_step.task_name == "test_flow"
|
|
303
|
+
assert flow_step.when == "org_config.username == 'test@example.com'"
|
|
304
|
+
assert flow_step.task_config == {"test": "value"}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class TestEvaluationMethods:
|
|
308
|
+
"""Test the evaluation methods for flow and task skipping."""
|
|
309
|
+
|
|
310
|
+
def setup_method(self):
|
|
311
|
+
"""Set up test fixtures."""
|
|
312
|
+
self.project_config = create_project_config("TestOwner", "TestRepo")
|
|
313
|
+
self.org_config = OrgConfig(
|
|
314
|
+
{"username": "test@example.com"}, "test", mock.Mock()
|
|
315
|
+
)
|
|
316
|
+
self.org_config.refresh_oauth_token = mock.Mock()
|
|
317
|
+
|
|
318
|
+
def test_evaluate_flow_step_with_true_condition(self):
|
|
319
|
+
"""Test _evaluate_flow_step with a condition that evaluates to True."""
|
|
320
|
+
flow_config = FlowConfig({"description": "Test Flow", "steps": {}})
|
|
321
|
+
flow_config.project_config = self.project_config
|
|
322
|
+
coordinator = FlowCoordinator(self.project_config, flow_config)
|
|
323
|
+
coordinator.org_config = self.org_config
|
|
324
|
+
|
|
325
|
+
step = FlowStepSpec(
|
|
326
|
+
task_config={},
|
|
327
|
+
step_num="1.0",
|
|
328
|
+
task_name="test_flow",
|
|
329
|
+
task_class=None,
|
|
330
|
+
project_config=self.project_config,
|
|
331
|
+
allow_failure=False,
|
|
332
|
+
when="org_config.username == 'test@example.com'",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
result = coordinator._evaluate_flow_step(step)
|
|
336
|
+
assert result is True
|
|
337
|
+
|
|
338
|
+
def test_evaluate_flow_step_with_false_condition(self):
|
|
339
|
+
"""Test _evaluate_flow_step with a condition that evaluates to False."""
|
|
340
|
+
flow_config = FlowConfig({"description": "Test Flow", "steps": {}})
|
|
341
|
+
flow_config.project_config = self.project_config
|
|
342
|
+
coordinator = FlowCoordinator(self.project_config, flow_config)
|
|
343
|
+
coordinator.org_config = self.org_config
|
|
344
|
+
|
|
345
|
+
step = FlowStepSpec(
|
|
346
|
+
task_config={},
|
|
347
|
+
step_num="1.0",
|
|
348
|
+
task_name="test_flow",
|
|
349
|
+
task_class=None,
|
|
350
|
+
project_config=self.project_config,
|
|
351
|
+
allow_failure=False,
|
|
352
|
+
when="org_config.username == 'wrong@example.com'",
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
result = coordinator._evaluate_flow_step(step)
|
|
356
|
+
assert result is False
|
|
357
|
+
|
|
358
|
+
def test_evaluate_flow_step_without_when_condition(self):
|
|
359
|
+
"""Test _evaluate_flow_step without a when condition."""
|
|
360
|
+
flow_config = FlowConfig({"description": "Test Flow", "steps": {}})
|
|
361
|
+
flow_config.project_config = self.project_config
|
|
362
|
+
coordinator = FlowCoordinator(self.project_config, flow_config)
|
|
363
|
+
coordinator.org_config = self.org_config
|
|
364
|
+
|
|
365
|
+
step = FlowStepSpec(
|
|
366
|
+
task_config={},
|
|
367
|
+
step_num="1.0",
|
|
368
|
+
task_name="test_flow",
|
|
369
|
+
task_class=None,
|
|
370
|
+
project_config=self.project_config,
|
|
371
|
+
allow_failure=False,
|
|
372
|
+
when=None,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
result = coordinator._evaluate_flow_step(step)
|
|
376
|
+
assert result is True
|
|
377
|
+
|
|
378
|
+
def test_is_task_in_skipped_flow_true(self):
|
|
379
|
+
"""Test _is_task_in_skipped_flow returns True when task is in skipped flow."""
|
|
380
|
+
flow_config = FlowConfig({"description": "Test Flow", "steps": {}})
|
|
381
|
+
flow_config.project_config = self.project_config
|
|
382
|
+
coordinator = FlowCoordinator(self.project_config, flow_config)
|
|
383
|
+
coordinator.org_config = self.org_config
|
|
384
|
+
|
|
385
|
+
skipped_flows_set = {"skipped_flow", "another_flow"}
|
|
386
|
+
task_path = "skipped_flow.sub_task"
|
|
387
|
+
|
|
388
|
+
result = coordinator._is_task_in_skipped_flow(task_path, skipped_flows_set)
|
|
389
|
+
assert result is True
|
|
390
|
+
|
|
391
|
+
def test_is_task_in_skipped_flow_false(self):
|
|
392
|
+
"""Test _is_task_in_skipped_flow returns False when task is not in skipped flow."""
|
|
393
|
+
flow_config = FlowConfig({"description": "Test Flow", "steps": {}})
|
|
394
|
+
flow_config.project_config = self.project_config
|
|
395
|
+
coordinator = FlowCoordinator(self.project_config, flow_config)
|
|
396
|
+
coordinator.org_config = self.org_config
|
|
397
|
+
|
|
398
|
+
skipped_flows_set = {"skipped_flow", "another_flow"}
|
|
399
|
+
task_path = "normal_flow.sub_task"
|
|
400
|
+
|
|
401
|
+
result = coordinator._is_task_in_skipped_flow(task_path, skipped_flows_set)
|
|
402
|
+
assert result is False
|
|
403
|
+
|
|
404
|
+
def test_is_task_in_skipped_flow_empty_set(self):
|
|
405
|
+
"""Test _is_task_in_skipped_flow with empty skipped flows set."""
|
|
406
|
+
flow_config = FlowConfig({"description": "Test Flow", "steps": {}})
|
|
407
|
+
flow_config.project_config = self.project_config
|
|
408
|
+
coordinator = FlowCoordinator(self.project_config, flow_config)
|
|
409
|
+
coordinator.org_config = self.org_config
|
|
410
|
+
|
|
411
|
+
skipped_flows_set = set()
|
|
412
|
+
task_path = "any_flow.sub_task"
|
|
413
|
+
|
|
414
|
+
result = coordinator._is_task_in_skipped_flow(task_path, skipped_flows_set)
|
|
415
|
+
assert result is False
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class TestExpressionCaching:
|
|
419
|
+
"""Test Jinja2 expression caching functionality."""
|
|
420
|
+
|
|
421
|
+
def setup_method(self):
|
|
422
|
+
"""Set up test fixtures."""
|
|
423
|
+
self.project_config = create_project_config("TestOwner", "TestRepo")
|
|
424
|
+
self.org_config = OrgConfig(
|
|
425
|
+
{"username": "test@example.com"}, "test", mock.Mock()
|
|
426
|
+
)
|
|
427
|
+
self.org_config.refresh_oauth_token = mock.Mock()
|
|
428
|
+
|
|
429
|
+
def test_expression_caching_reuse(self):
|
|
430
|
+
"""Test that compiled expressions are cached and reused."""
|
|
431
|
+
flow_config = FlowConfig({"description": "Test Flow", "steps": {}})
|
|
432
|
+
flow_config.project_config = self.project_config
|
|
433
|
+
coordinator = FlowCoordinator(self.project_config, flow_config)
|
|
434
|
+
coordinator.org_config = self.org_config
|
|
435
|
+
|
|
436
|
+
# Clear any existing cache
|
|
437
|
+
coordinator._expression_cache = {}
|
|
438
|
+
|
|
439
|
+
step1 = FlowStepSpec(
|
|
440
|
+
task_config={},
|
|
441
|
+
step_num="1.0",
|
|
442
|
+
task_name="test_flow1",
|
|
443
|
+
task_class=None,
|
|
444
|
+
project_config=self.project_config,
|
|
445
|
+
allow_failure=False,
|
|
446
|
+
when="org_config.username == 'test@example.com'",
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
step2 = FlowStepSpec(
|
|
450
|
+
task_config={},
|
|
451
|
+
step_num="2.0",
|
|
452
|
+
task_name="test_flow2",
|
|
453
|
+
task_class=None,
|
|
454
|
+
project_config=self.project_config,
|
|
455
|
+
allow_failure=False,
|
|
456
|
+
when="org_config.username == 'test@example.com'",
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# First evaluation should compile and cache the expression
|
|
460
|
+
result1 = coordinator._evaluate_flow_step(step1)
|
|
461
|
+
assert result1 is True
|
|
462
|
+
assert len(coordinator._expression_cache) == 1
|
|
463
|
+
|
|
464
|
+
# Second evaluation should use cached expression
|
|
465
|
+
result2 = coordinator._evaluate_flow_step(step2)
|
|
466
|
+
assert result2 is True
|
|
467
|
+
assert (
|
|
468
|
+
len(coordinator._expression_cache) == 1
|
|
469
|
+
) # Still only one cached expression
|
|
470
|
+
|
|
471
|
+
def test_expression_caching_different_expressions(self):
|
|
472
|
+
"""Test that different expressions are cached separately."""
|
|
473
|
+
flow_config = FlowConfig({"description": "Test Flow", "steps": {}})
|
|
474
|
+
flow_config.project_config = self.project_config
|
|
475
|
+
coordinator = FlowCoordinator(self.project_config, flow_config)
|
|
476
|
+
coordinator.org_config = self.org_config
|
|
477
|
+
|
|
478
|
+
# Clear any existing cache
|
|
479
|
+
coordinator._expression_cache = {}
|
|
480
|
+
|
|
481
|
+
step1 = FlowStepSpec(
|
|
482
|
+
task_config={},
|
|
483
|
+
step_num="1.0",
|
|
484
|
+
task_name="test_flow1",
|
|
485
|
+
task_class=None,
|
|
486
|
+
project_config=self.project_config,
|
|
487
|
+
allow_failure=False,
|
|
488
|
+
when="org_config.username == 'test@example.com'",
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
step2 = FlowStepSpec(
|
|
492
|
+
task_config={},
|
|
493
|
+
step_num="2.0",
|
|
494
|
+
task_name="test_flow2",
|
|
495
|
+
task_class=None,
|
|
496
|
+
project_config=self.project_config,
|
|
497
|
+
allow_failure=False,
|
|
498
|
+
when="org_config.username == 'wrong@example.com'",
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Evaluate both steps
|
|
502
|
+
coordinator._evaluate_flow_step(step1)
|
|
503
|
+
coordinator._evaluate_flow_step(step2)
|
|
504
|
+
|
|
505
|
+
# Should have two different cached expressions
|
|
506
|
+
assert len(coordinator._expression_cache) == 2
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class TestPerformanceImprovements:
|
|
510
|
+
"""Test that performance improvements work correctly."""
|
|
511
|
+
|
|
512
|
+
def setup_method(self):
|
|
513
|
+
"""Set up test fixtures."""
|
|
514
|
+
self.project_config = create_project_config("TestOwner", "TestRepo")
|
|
515
|
+
self.org_config = OrgConfig(
|
|
516
|
+
{"username": "test@example.com"}, "test", mock.Mock()
|
|
517
|
+
)
|
|
518
|
+
self.org_config.refresh_oauth_token = mock.Mock()
|
|
519
|
+
|
|
520
|
+
def test_context_reuse(self):
|
|
521
|
+
"""Test that Jinja2 context is reused when possible."""
|
|
522
|
+
flow_config = FlowConfig({"description": "Test Flow", "steps": {}})
|
|
523
|
+
flow_config.project_config = self.project_config
|
|
524
|
+
coordinator = FlowCoordinator(self.project_config, flow_config)
|
|
525
|
+
coordinator.org_config = self.org_config
|
|
526
|
+
|
|
527
|
+
# Clear any existing context
|
|
528
|
+
coordinator._jinja2_context = None
|
|
529
|
+
coordinator._context_project_config = None
|
|
530
|
+
coordinator._context_org_config = None
|
|
531
|
+
|
|
532
|
+
step = FlowStepSpec(
|
|
533
|
+
task_config={},
|
|
534
|
+
step_num="1.0",
|
|
535
|
+
task_name="test_flow",
|
|
536
|
+
task_class=None,
|
|
537
|
+
project_config=self.project_config,
|
|
538
|
+
allow_failure=False,
|
|
539
|
+
when="org_config.username == 'test@example.com'",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# First evaluation should create context
|
|
543
|
+
result1 = coordinator._evaluate_flow_step(step)
|
|
544
|
+
assert result1 is True
|
|
545
|
+
assert coordinator._jinja2_context is not None
|
|
546
|
+
assert coordinator._context_project_config == self.project_config
|
|
547
|
+
assert coordinator._context_org_config == self.org_config
|
|
548
|
+
|
|
549
|
+
# Second evaluation should reuse context
|
|
550
|
+
original_context = coordinator._jinja2_context
|
|
551
|
+
result2 = coordinator._evaluate_flow_step(step)
|
|
552
|
+
assert result2 is True
|
|
553
|
+
assert coordinator._jinja2_context is original_context # Same object reused
|
cumulusci/core/flowrunner.py
CHANGED
|
@@ -164,6 +164,11 @@ class StepSpec:
|
|
|
164
164
|
)
|
|
165
165
|
|
|
166
166
|
|
|
167
|
+
class FlowStepSpec(StepSpec):
|
|
168
|
+
def __init__(self, *args, **kwargs):
|
|
169
|
+
super().__init__(*args, **kwargs)
|
|
170
|
+
|
|
171
|
+
|
|
167
172
|
class StepResult(NamedTuple):
|
|
168
173
|
step_num: StepVersion
|
|
169
174
|
task_name: str
|
|
@@ -356,6 +361,10 @@ class FlowCoordinator:
|
|
|
356
361
|
|
|
357
362
|
self.logger = self._init_logger()
|
|
358
363
|
self.steps = self._init_steps()
|
|
364
|
+
self._expression_cache = {}
|
|
365
|
+
self._jinja2_context = None
|
|
366
|
+
self._context_project_config = None
|
|
367
|
+
self._context_org_config = None
|
|
359
368
|
|
|
360
369
|
@classmethod
|
|
361
370
|
def from_steps(
|
|
@@ -403,6 +412,9 @@ class FlowCoordinator:
|
|
|
403
412
|
previous_parts = []
|
|
404
413
|
previous_source = None
|
|
405
414
|
for step in self.steps:
|
|
415
|
+
if isinstance(step, FlowStepSpec):
|
|
416
|
+
continue
|
|
417
|
+
|
|
406
418
|
parts = step.path.split(".")
|
|
407
419
|
steps = str(step.step_num).split("/")
|
|
408
420
|
if len(parts) > len(steps):
|
|
@@ -491,7 +503,27 @@ class FlowCoordinator:
|
|
|
491
503
|
self._rule(new_line=True)
|
|
492
504
|
|
|
493
505
|
try:
|
|
506
|
+
# Pre-evaluate all flow conditions
|
|
507
|
+
skipped_flows_set = set()
|
|
494
508
|
for step in self.steps:
|
|
509
|
+
if isinstance(step, FlowStepSpec):
|
|
510
|
+
if not self._evaluate_flow_step(step):
|
|
511
|
+
skipped_flows_set.add(step.path)
|
|
512
|
+
|
|
513
|
+
# Main execution loop with optimized path checking
|
|
514
|
+
for step in self.steps:
|
|
515
|
+
if isinstance(step, FlowStepSpec):
|
|
516
|
+
self.logger.info(
|
|
517
|
+
f"Skipping Flow {step.task_name} (skipped unless {step.when})"
|
|
518
|
+
)
|
|
519
|
+
continue
|
|
520
|
+
|
|
521
|
+
if self._is_task_in_skipped_flow(step.path, skipped_flows_set):
|
|
522
|
+
self.logger.info(
|
|
523
|
+
f"Skipping Task {step.task_name} in flow {step.path} (parent flow is skipped)"
|
|
524
|
+
)
|
|
525
|
+
continue
|
|
526
|
+
|
|
495
527
|
self._run_step(step)
|
|
496
528
|
flow_name = f"'{self.name}' " if self.name else ""
|
|
497
529
|
self.logger.info(
|
|
@@ -500,6 +532,45 @@ class FlowCoordinator:
|
|
|
500
532
|
finally:
|
|
501
533
|
self.callbacks.post_flow(self)
|
|
502
534
|
|
|
535
|
+
def _get_jinja2_context(self, project_config, org_config):
|
|
536
|
+
"""Get or create jinja2 context, reusing when possible."""
|
|
537
|
+
if (
|
|
538
|
+
self._jinja2_context is None
|
|
539
|
+
or self._context_project_config != project_config
|
|
540
|
+
or self._context_org_config != org_config
|
|
541
|
+
):
|
|
542
|
+
|
|
543
|
+
self._jinja2_context = {
|
|
544
|
+
"project_config": project_config,
|
|
545
|
+
"org_config": org_config,
|
|
546
|
+
}
|
|
547
|
+
self._context_project_config = project_config
|
|
548
|
+
self._context_org_config = org_config
|
|
549
|
+
|
|
550
|
+
return self._jinja2_context
|
|
551
|
+
|
|
552
|
+
def _evaluate_flow_step(self, step: StepSpec) -> bool:
|
|
553
|
+
if not step.when:
|
|
554
|
+
return True
|
|
555
|
+
|
|
556
|
+
# Check cache first
|
|
557
|
+
if step.when in self._expression_cache:
|
|
558
|
+
expr = self._expression_cache[step.when]
|
|
559
|
+
else:
|
|
560
|
+
expr = jinja2_env.compile_expression(step.when)
|
|
561
|
+
self._expression_cache[step.when] = expr
|
|
562
|
+
|
|
563
|
+
jinja2_context = self._get_jinja2_context(step.project_config, self.org_config)
|
|
564
|
+
|
|
565
|
+
return expr(**jinja2_context)
|
|
566
|
+
|
|
567
|
+
def _is_task_in_skipped_flow(self, task_path: str, skipped_flows_set: set) -> bool:
|
|
568
|
+
"""Check if task belongs to any skipped flow using O(1) set lookup."""
|
|
569
|
+
for skipped_path in skipped_flows_set:
|
|
570
|
+
if task_path.startswith(skipped_path + "."):
|
|
571
|
+
return True
|
|
572
|
+
return False
|
|
573
|
+
|
|
503
574
|
def _run_step(self, step: StepSpec):
|
|
504
575
|
if step.skip:
|
|
505
576
|
self._rule(fill="*")
|
|
@@ -508,12 +579,7 @@ class FlowCoordinator:
|
|
|
508
579
|
return
|
|
509
580
|
|
|
510
581
|
if step.when:
|
|
511
|
-
|
|
512
|
-
"project_config": step.project_config,
|
|
513
|
-
"org_config": self.org_config,
|
|
514
|
-
}
|
|
515
|
-
expr = jinja2_env.compile_expression(step.when)
|
|
516
|
-
value = expr(**jinja2_context)
|
|
582
|
+
value = self._evaluate_flow_step(step)
|
|
517
583
|
if not value:
|
|
518
584
|
self.logger.info(
|
|
519
585
|
f"Skipping task {step.task_name} (skipped unless {step.when})"
|
|
@@ -681,8 +747,24 @@ class FlowCoordinator:
|
|
|
681
747
|
else:
|
|
682
748
|
path = name
|
|
683
749
|
step_options = step_config.get("options", {})
|
|
750
|
+
parent_task_options = parent_options.get(name, {})
|
|
751
|
+
step_options.update(parent_task_options)
|
|
684
752
|
step_ui_options = step_config.get("ui_options", {})
|
|
685
753
|
flow_config = project_config.get_flow(name)
|
|
754
|
+
|
|
755
|
+
if step_config.get("when"):
|
|
756
|
+
visited_steps.append(
|
|
757
|
+
FlowStepSpec(
|
|
758
|
+
task_config={},
|
|
759
|
+
step_num=step_number,
|
|
760
|
+
task_name=path,
|
|
761
|
+
task_class=None,
|
|
762
|
+
project_config=flow_config.project_config,
|
|
763
|
+
allow_failure=step_config.get("ignore_failure", False),
|
|
764
|
+
when=step_config.get("when"),
|
|
765
|
+
)
|
|
766
|
+
)
|
|
767
|
+
|
|
686
768
|
for sub_number, sub_stepconf in flow_config.steps.items():
|
|
687
769
|
# append the flow number to the child number, since its a LooseVersion.
|
|
688
770
|
# e.g. if we're in step 2.3 which references a flow with steps 1-5, it
|
cumulusci/cumulusci.yml
CHANGED
|
@@ -831,6 +831,18 @@ tasks:
|
|
|
831
831
|
class_path: cumulusci.tasks.salesforce.SfDataCommands.DataCreateRecordTask
|
|
832
832
|
description: "Executes the `sf data create` command against an org"
|
|
833
833
|
group: SalesforceDX Data Commands
|
|
834
|
+
update_package_version:
|
|
835
|
+
class_path: cumulusci.tasks.salesforce.SfPackageCommands.PackageVersionUpdateTask
|
|
836
|
+
description: "Executes the `sf package version update` command against an org"
|
|
837
|
+
group: Salesforce Packages
|
|
838
|
+
get_package_version:
|
|
839
|
+
description: Get package version id from package name and version
|
|
840
|
+
class_path: cumulusci.tasks.salesforce.getPackageVersion.GetPackageVersion
|
|
841
|
+
group: Salesforce Packages
|
|
842
|
+
options:
|
|
843
|
+
package_name: $project_config.project__package__name
|
|
844
|
+
fail_on_error: False
|
|
845
|
+
|
|
834
846
|
flows:
|
|
835
847
|
ci_beta:
|
|
836
848
|
group: Continuous Integration
|
|
@@ -29,7 +29,7 @@ from cumulusci.core.exceptions import (
|
|
|
29
29
|
VcsException,
|
|
30
30
|
)
|
|
31
31
|
from cumulusci.core.sfdx import convert_sfdx_source
|
|
32
|
-
from cumulusci.core.utils import process_bool_arg
|
|
32
|
+
from cumulusci.core.utils import process_bool_arg, process_list_arg
|
|
33
33
|
from cumulusci.core.versions import PackageType, PackageVersionNumber, VersionTypeEnum
|
|
34
34
|
from cumulusci.salesforce_api.package_zip import (
|
|
35
35
|
BasePackageZipBuilder,
|
|
@@ -114,15 +114,21 @@ class CreatePackageVersion(BaseSalesforceApiTask):
|
|
|
114
114
|
"version_base": {
|
|
115
115
|
"description": "The version number to use as a base before incrementing. "
|
|
116
116
|
"Optional; defaults to the highest existing version number of this package. "
|
|
117
|
-
"Can be set to ``latest_vcs_release`` to use the version of the most recent release published to
|
|
117
|
+
"Can be set to ``latest_vcs_release`` to use the version of the most recent release published to VCS."
|
|
118
|
+
"If version_number is set, version_base and version_type will be ignored"
|
|
118
119
|
},
|
|
119
120
|
"version_type": {
|
|
120
121
|
"description": "The part of the version number to increment. "
|
|
121
122
|
"Options are major, minor, patch, build. Defaults to build"
|
|
123
|
+
"If version_number is set, version_base and version_type will be ignored"
|
|
124
|
+
},
|
|
125
|
+
"version_number": {
|
|
126
|
+
"description": "Set a fixed version number, if not using version_base and version_type"
|
|
122
127
|
},
|
|
123
128
|
"skip_validation": {
|
|
124
129
|
"description": "If true, skip validation of the package version. Default: false. "
|
|
125
130
|
"Skipping validation creates packages more quickly, but they cannot be promoted for release."
|
|
131
|
+
"And package version is created without reference to dependencies."
|
|
126
132
|
},
|
|
127
133
|
"org_dependent": {
|
|
128
134
|
"description": "If true, create an org-dependent unlocked package. Default: false."
|
|
@@ -156,6 +162,11 @@ class CreatePackageVersion(BaseSalesforceApiTask):
|
|
|
156
162
|
"description": "If True, create unlocked packages for unpackaged metadata in this project and dependencies. "
|
|
157
163
|
"Defaults to False."
|
|
158
164
|
},
|
|
165
|
+
"dependencies": {
|
|
166
|
+
"description": "The list of dependencies to use when creating the package version. Defaults to None."
|
|
167
|
+
"If not provided, the dependencies will be resolved using the resolution_strategy."
|
|
168
|
+
"The format should be a pcakge version Ids i.e '04t...,04t...'"
|
|
169
|
+
},
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
def _init_options(self, kwargs):
|
|
@@ -200,6 +211,21 @@ class CreatePackageVersion(BaseSalesforceApiTask):
|
|
|
200
211
|
self.options["create_unlocked_dependency_packages"] = process_bool_arg(
|
|
201
212
|
self.options.get("create_unlocked_dependency_packages") or False
|
|
202
213
|
)
|
|
214
|
+
self.options["version_number"] = (
|
|
215
|
+
PackageVersionNumber.parse(
|
|
216
|
+
self.options.get("version_number"), package_type=PackageType.SECOND_GEN
|
|
217
|
+
)
|
|
218
|
+
if self.options.get("version_number")
|
|
219
|
+
else None
|
|
220
|
+
)
|
|
221
|
+
self.options["dependencies"] = (
|
|
222
|
+
[
|
|
223
|
+
{"subscriberPackageVersionId": x}
|
|
224
|
+
for x in process_list_arg(self.options.get("dependencies"))
|
|
225
|
+
]
|
|
226
|
+
if self.options.get("dependencies")
|
|
227
|
+
else None
|
|
228
|
+
)
|
|
203
229
|
|
|
204
230
|
def _init_task(self):
|
|
205
231
|
self.tooling = get_simple_salesforce_connection(
|
|
@@ -384,9 +410,13 @@ class CreatePackageVersion(BaseSalesforceApiTask):
|
|
|
384
410
|
return res["records"][0]["Id"]
|
|
385
411
|
|
|
386
412
|
# Create the package descriptor
|
|
387
|
-
version_number = self.
|
|
413
|
+
version_number = self.options.get(
|
|
414
|
+
"version_number"
|
|
415
|
+
) or self._get_base_version_number(
|
|
388
416
|
package_config.version_base, package_id
|
|
389
|
-
).increment(
|
|
417
|
+
).increment(
|
|
418
|
+
package_config.version_type
|
|
419
|
+
)
|
|
390
420
|
|
|
391
421
|
package_descriptor = {
|
|
392
422
|
"id": package_id,
|
|
@@ -442,7 +472,9 @@ class CreatePackageVersion(BaseSalesforceApiTask):
|
|
|
442
472
|
and not is_dependency
|
|
443
473
|
):
|
|
444
474
|
self.logger.info("Determining dependencies for package")
|
|
445
|
-
dependencies =
|
|
475
|
+
dependencies = (
|
|
476
|
+
self.options.get("dependencies") or self._get_dependencies()
|
|
477
|
+
)
|
|
446
478
|
if dependencies:
|
|
447
479
|
package_descriptor["dependencies"] = dependencies
|
|
448
480
|
|