cumulusci-plus 5.0.5__py3-none-any.whl → 5.0.8__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "5.0.5"
1
+ __version__ = "5.0.8"
cumulusci/cli/cci.py CHANGED
@@ -1,7 +1,9 @@
1
1
  import code
2
2
  import contextlib
3
+ import os
3
4
  import pdb
4
5
  import runpy
6
+ import signal
5
7
  import sys
6
8
  import traceback
7
9
 
@@ -43,6 +45,71 @@ SUGGEST_ERROR_COMMAND = (
43
45
 
44
46
  USAGE_ERRORS = (CumulusCIUsageError, click.UsageError)
45
47
 
48
+ # Global variable to track the context stack for cleanup on signal
49
+ _exit_stack = None
50
+ _signal_handler_active = False # Flag to prevent recursive signal handler calls
51
+
52
+
53
+ def _signal_handler(signum, frame):
54
+ """Handle termination signals.
55
+
56
+ This handler ensures the CLI exits gracefully with a failure code when
57
+ receiving SIGTERM or SIGINT signals, such as when an Azure DevOps pipeline
58
+ is cancelled. It also terminates all child processes in the process group.
59
+ """
60
+ global _signal_handler_active
61
+
62
+ # Prevent recursive signal handler calls
63
+ if _signal_handler_active:
64
+ return
65
+
66
+ _signal_handler_active = True
67
+
68
+ console = Console()
69
+
70
+ signal_names = {signal.SIGTERM: "SIGTERM", signal.SIGINT: "SIGINT"}
71
+
72
+ signal_name = signal_names.get(signum, f"signal {signum}")
73
+
74
+ console.print(
75
+ f"\n[yellow]Received {signal_name} - CumulusCI is being terminated[/yellow]"
76
+ )
77
+ console.print(
78
+ "[yellow]Exiting with failure code due to external cancellation.[/yellow]"
79
+ )
80
+
81
+ # Clean up managed resources
82
+ if _exit_stack:
83
+ try:
84
+ _exit_stack.close()
85
+ except Exception as e:
86
+ console.print(f"[red]Error during cleanup: {e}[/red]")
87
+
88
+ # Terminate child processes in the process group
89
+ try:
90
+ console.print("[yellow]Terminating child processes...[/yellow]")
91
+
92
+ # Temporarily ignore the signal to prevent recursion
93
+ old_handler = signal.signal(signum, signal.SIG_IGN)
94
+
95
+ try:
96
+ pgrp = os.getpgrp()
97
+ # Send signal to all processes in the group except ourselves
98
+ os.killpg(pgrp, signum)
99
+ finally:
100
+ # Restore the original signal handler
101
+ signal.signal(signum, old_handler)
102
+
103
+ except ProcessLookupError:
104
+ # Process group may not exist or may already be terminated
105
+ pass
106
+ except Exception as e:
107
+ console.print(f"[red]Warning: Error terminating child processes: {e}[/red]")
108
+
109
+ # Exit with appropriate failure code
110
+ exit_code = 143 if signum == signal.SIGTERM else 130 # Standard exit codes
111
+ sys.exit(exit_code)
112
+
46
113
 
47
114
  #
48
115
  # Root command
@@ -54,7 +121,25 @@ def main(args=None):
54
121
 
55
122
  This wraps the `click` library in order to do some initialization and centralized error handling.
56
123
  """
124
+ global _exit_stack
125
+
126
+ # Create a new process group so we can terminate all child processes
127
+ # when we receive a termination signal
128
+ try:
129
+ if hasattr(os, "setpgrp"):
130
+ # On Unix systems, create a new process group
131
+ os.setpgrp()
132
+ except Exception:
133
+ # On Windows or if setpgrp fails, continue without process group
134
+ pass
135
+
136
+ # Set up signal handlers for graceful termination
137
+ signal.signal(signal.SIGTERM, _signal_handler)
138
+ signal.signal(signal.SIGINT, _signal_handler)
139
+
57
140
  with contextlib.ExitStack() as stack:
141
+ _exit_stack = stack # Store reference for signal handler cleanup
142
+
58
143
  args = args or sys.argv
59
144
 
60
145
  # (If enabled) set up requests to validate certs using system CA certs instead of certifi
@@ -98,15 +183,20 @@ def main(args=None):
98
183
  sys.exit(1)
99
184
  except Exception as e:
100
185
  if debug:
186
+ console = Console()
101
187
  show_debug_info()
188
+ console.print(
189
+ f"\n[red bold]Debug info for bug reports:\n{traceback.format_exc()}"
190
+ )
191
+ sys.exit(1)
102
192
  else:
103
193
  handle_exception(
104
- e,
105
- is_error_command,
106
- tempfile_path,
107
- should_show_stacktraces,
194
+ e, is_error_command, tempfile_path, should_show_stacktraces
108
195
  )
109
- sys.exit(1)
196
+ sys.exit(1)
197
+
198
+ # Clear the global reference when exiting normally
199
+ _exit_stack = None
110
200
 
111
201
 
112
202
  def handle_exception(
@@ -1,7 +1,7 @@
1
1
  import contextlib
2
2
  import io
3
3
  import os
4
- import shutil
4
+ import signal
5
5
  import sys
6
6
  import tempfile
7
7
  from pathlib import Path
@@ -29,19 +29,14 @@ CONSOLE = mock.Mock()
29
29
  def env_config():
30
30
  config = {
31
31
  "global_tempdir": tempfile.gettempdir(),
32
- "tempdir": tempfile.mkdtemp(),
33
- "environ_mock": mock.patch.dict(
34
- os.environ, {"HOME": tempfile.mkdtemp(), "CUMULUSCI_KEY": ""}
35
- ),
32
+ "tempdir": tempfile.gettempdir(),
36
33
  }
37
- # setup
38
- config["environ_mock"].start()
39
- assert config["global_tempdir"] in os.environ["HOME"]
40
- yield config
41
- # tear down
42
- assert config["global_tempdir"] in os.environ["HOME"]
43
- config["environ_mock"].stop()
44
- shutil.rmtree(config["tempdir"])
34
+
35
+ # Reset signal handler flag to ensure clean state for tests
36
+ cci._signal_handler_active = False
37
+
38
+ with mock.patch.dict(os.environ, config):
39
+ yield
45
40
 
46
41
 
47
42
  @mock.patch("cumulusci.cli.cci.tee_stdout_stderr")
@@ -266,7 +261,9 @@ def test_cci_org_default__no_orgname(
266
261
  DEPLOY_CLASS_PATH = f"cumulusci.tasks.salesforce.Deploy{'.Deploy' if sys.version_info >= (3, 11) else ''}"
267
262
 
268
263
 
269
- @mock.patch("cumulusci.cli.cci.init_logger", mock.Mock())
264
+ @mock.patch(
265
+ "cumulusci.cli.cci.init_logger", mock.Mock()
266
+ ) # side effects break other tests
270
267
  @mock.patch("cumulusci.cli.cci.tee_stdout_stderr", mock.MagicMock())
271
268
  @mock.patch(f"{DEPLOY_CLASS_PATH}.__call__", mock.Mock())
272
269
  @mock.patch("sys.exit", mock.Mock())
@@ -543,3 +540,246 @@ def test_dash_dash_version(
543
540
 
544
541
  cci.main(["cci", "--version"])
545
542
  assert len(show_version_info.mock_calls) == 2
543
+
544
+
545
+ @mock.patch("sys.exit")
546
+ @mock.patch("cumulusci.cli.cci.Console")
547
+ @mock.patch("os.killpg")
548
+ @mock.patch("os.getpgrp")
549
+ @mock.patch("signal.signal")
550
+ def test_signal_handler_terminates_process_group(
551
+ mock_signal, mock_getpgrp, mock_killpg, mock_console, mock_exit
552
+ ):
553
+ """Test that the signal handler terminates the process group"""
554
+ console_instance = mock_console.return_value
555
+ mock_getpgrp.return_value = 1234 # Mock process group ID
556
+
557
+ # Mock the global exit stack
558
+ mock_exit_stack = mock.Mock()
559
+ with mock.patch.object(cci, "_exit_stack", mock_exit_stack):
560
+ # Call the signal handler with SIGTERM
561
+ cci._signal_handler(signal.SIGTERM, None)
562
+
563
+ # Verify console output
564
+ console_instance.print.assert_any_call(
565
+ "\n[yellow]Received SIGTERM - CumulusCI is being terminated[/yellow]"
566
+ )
567
+ console_instance.print.assert_any_call(
568
+ "[yellow]Exiting with failure code due to external cancellation.[/yellow]"
569
+ )
570
+ console_instance.print.assert_any_call(
571
+ "[yellow]Terminating child processes...[/yellow]"
572
+ )
573
+
574
+ # Verify cleanup was called
575
+ mock_exit_stack.close.assert_called_once()
576
+
577
+ # Verify signal was temporarily ignored and then restored
578
+ mock_signal.assert_called()
579
+
580
+ # Verify process group was terminated
581
+ mock_getpgrp.assert_called_once()
582
+ mock_killpg.assert_called_once_with(1234, signal.SIGTERM)
583
+
584
+ # Verify exit with correct code
585
+ mock_exit.assert_called_once_with(143)
586
+
587
+
588
+ @mock.patch("sys.exit")
589
+ @mock.patch("cumulusci.cli.cci.Console")
590
+ @mock.patch("os.killpg")
591
+ @mock.patch("os.getpgrp")
592
+ @mock.patch("signal.signal")
593
+ def test_signal_handler_sigint(
594
+ mock_signal, mock_getpgrp, mock_killpg, mock_console, mock_exit
595
+ ):
596
+ """Test that the signal handler properly handles SIGINT"""
597
+ console_instance = mock_console.return_value
598
+ mock_getpgrp.return_value = 1234
599
+
600
+ # Mock the global exit stack
601
+ mock_exit_stack = mock.Mock()
602
+ with mock.patch.object(cci, "_exit_stack", mock_exit_stack):
603
+ # Call the signal handler with SIGINT
604
+ cci._signal_handler(signal.SIGINT, None)
605
+
606
+ # Verify console output
607
+ console_instance.print.assert_any_call(
608
+ "\n[yellow]Received SIGINT - CumulusCI is being terminated[/yellow]"
609
+ )
610
+
611
+ # Verify process group was terminated with SIGINT
612
+ mock_killpg.assert_called_once_with(1234, signal.SIGINT)
613
+
614
+ # Verify exit with correct code for SIGINT
615
+ mock_exit.assert_called_once_with(130)
616
+
617
+
618
+ @mock.patch("sys.exit")
619
+ @mock.patch("cumulusci.cli.cci.Console")
620
+ def test_signal_handler_prevents_recursion(mock_console, mock_exit):
621
+ """Test that the signal handler prevents recursive calls"""
622
+ console_instance = mock_console.return_value
623
+
624
+ # Set the flag to simulate handler already active
625
+ with mock.patch.object(cci, "_signal_handler_active", True):
626
+ # Call the signal handler
627
+ cci._signal_handler(signal.SIGTERM, None)
628
+
629
+ # Verify no console output (handler should return immediately)
630
+ console_instance.print.assert_not_called()
631
+
632
+ # Verify no exit call
633
+ mock_exit.assert_not_called()
634
+
635
+
636
+ @mock.patch("sys.exit")
637
+ @mock.patch("cumulusci.cli.cci.Console")
638
+ @mock.patch("os.killpg")
639
+ @mock.patch("os.getpgrp")
640
+ @mock.patch("signal.signal")
641
+ def test_signal_handler_handles_killpg_error(
642
+ mock_signal, mock_getpgrp, mock_killpg, mock_console, mock_exit
643
+ ):
644
+ """Test that the signal handler handles errors from killpg gracefully"""
645
+ console_instance = mock_console.return_value
646
+ mock_getpgrp.return_value = 1234
647
+ mock_killpg.side_effect = OSError("Process group not found")
648
+
649
+ # Call the signal handler with SIGTERM
650
+ cci._signal_handler(signal.SIGTERM, None)
651
+
652
+ # Verify error message was printed
653
+ console_instance.print.assert_any_call(
654
+ "[red]Warning: Error terminating child processes: Process group not found[/red]"
655
+ )
656
+
657
+ # Verify it still exits with correct code
658
+ mock_exit.assert_called_once_with(143)
659
+
660
+
661
+ @mock.patch("sys.exit")
662
+ @mock.patch("cumulusci.cli.cci.Console")
663
+ def test_signal_handler_without_process_group_support(mock_console, mock_exit):
664
+ """Test that the signal handler works on systems without process group support"""
665
+ console_instance = mock_console.return_value
666
+
667
+ # Mock the global exit stack
668
+ mock_exit_stack = mock.Mock()
669
+
670
+ # Mock os module to not have killpg or getpgrp
671
+ with mock.patch.object(cci, "_exit_stack", mock_exit_stack):
672
+ with mock.patch.object(os, "killpg", None, create=True):
673
+ with mock.patch.object(os, "getpgrp", None, create=True):
674
+ # Call the signal handler with SIGTERM
675
+ cci._signal_handler(signal.SIGTERM, None)
676
+
677
+ # Verify basic functionality still works
678
+ console_instance.print.assert_any_call(
679
+ "\n[yellow]Received SIGTERM - CumulusCI is being terminated[/yellow]"
680
+ )
681
+ mock_exit.assert_called_once_with(143)
682
+
683
+
684
+ @mock.patch("os.setpgrp")
685
+ def test_main_creates_process_group(mock_setpgrp):
686
+ """Test that main() creates a new process group"""
687
+ # Mock dependencies to avoid actual CLI execution
688
+ with mock.patch.multiple(
689
+ "cumulusci.cli.cci",
690
+ signal=mock.Mock(),
691
+ init_requests_trust=mock.Mock(),
692
+ check_latest_version=mock.Mock(),
693
+ check_latest_plugins=mock.Mock(),
694
+ get_tempfile_logger=mock.Mock(return_value=(mock.Mock(), "tempfile.log")),
695
+ tee_stdout_stderr=mock.Mock(
696
+ return_value=mock.Mock(__enter__=mock.Mock(), __exit__=mock.Mock())
697
+ ),
698
+ set_debug_mode=mock.Mock(
699
+ return_value=mock.Mock(__enter__=mock.Mock(), __exit__=mock.Mock())
700
+ ),
701
+ CliRuntime=mock.Mock(),
702
+ init_logger=mock.Mock(),
703
+ cli=mock.Mock(),
704
+ ):
705
+ try:
706
+ cci.main(["cci", "version"])
707
+ except SystemExit:
708
+ pass # Expected for version command
709
+
710
+ # Verify process group was created
711
+ mock_setpgrp.assert_called_once()
712
+
713
+
714
+ @mock.patch("os.setpgrp")
715
+ def test_main_handles_setpgrp_error(mock_setpgrp):
716
+ """Test that main() handles setpgrp errors gracefully"""
717
+ mock_setpgrp.side_effect = OSError("Operation not permitted")
718
+
719
+ # Mock dependencies to avoid actual CLI execution
720
+ with mock.patch.multiple(
721
+ "cumulusci.cli.cci",
722
+ signal=mock.Mock(),
723
+ init_requests_trust=mock.Mock(),
724
+ check_latest_version=mock.Mock(),
725
+ check_latest_plugins=mock.Mock(),
726
+ get_tempfile_logger=mock.Mock(return_value=(mock.Mock(), "tempfile.log")),
727
+ tee_stdout_stderr=mock.Mock(
728
+ return_value=mock.Mock(__enter__=mock.Mock(), __exit__=mock.Mock())
729
+ ),
730
+ set_debug_mode=mock.Mock(
731
+ return_value=mock.Mock(__enter__=mock.Mock(), __exit__=mock.Mock())
732
+ ),
733
+ CliRuntime=mock.Mock(),
734
+ init_logger=mock.Mock(),
735
+ cli=mock.Mock(),
736
+ ):
737
+ try:
738
+ # Should not raise an exception even if setpgrp fails
739
+ cci.main(["cci", "version"])
740
+ except SystemExit:
741
+ pass # Expected for version command
742
+ except OSError:
743
+ pytest.fail("main() should handle setpgrp errors gracefully")
744
+
745
+ # Verify setpgrp was attempted
746
+ mock_setpgrp.assert_called_once()
747
+
748
+
749
+ @mock.patch("signal.signal")
750
+ def test_main_registers_signal_handlers(mock_signal):
751
+ """Test that main() registers signal handlers"""
752
+ # Create a proper context manager mock
753
+ context_manager_mock = mock.Mock()
754
+ context_manager_mock.__enter__ = mock.Mock(return_value=context_manager_mock)
755
+ context_manager_mock.__exit__ = mock.Mock(return_value=None)
756
+
757
+ # Create another context manager mock for set_debug_mode
758
+ debug_context_mock = mock.Mock()
759
+ debug_context_mock.__enter__ = mock.Mock(return_value=debug_context_mock)
760
+ debug_context_mock.__exit__ = mock.Mock(return_value=None)
761
+
762
+ # Mock dependencies to avoid actual CLI execution
763
+ with mock.patch.multiple(
764
+ "cumulusci.cli.cci",
765
+ init_requests_trust=mock.Mock(),
766
+ check_latest_version=mock.Mock(),
767
+ check_latest_plugins=mock.Mock(),
768
+ get_tempfile_logger=mock.Mock(return_value=(mock.Mock(), "tempfile.log")),
769
+ tee_stdout_stderr=mock.Mock(return_value=context_manager_mock),
770
+ set_debug_mode=mock.Mock(return_value=debug_context_mock),
771
+ CliRuntime=mock.Mock(),
772
+ init_logger=mock.Mock(),
773
+ cli=mock.Mock(),
774
+ ):
775
+ try:
776
+ cci.main(["cci", "version"])
777
+ except SystemExit:
778
+ pass # Expected for version command
779
+
780
+ # Verify signal handlers were registered
781
+ expected_calls = [
782
+ mock.call(signal.SIGTERM, cci._signal_handler),
783
+ mock.call(signal.SIGINT, cci._signal_handler),
784
+ ]
785
+ mock_signal.assert_has_calls(expected_calls, any_order=True)
@@ -1,5 +1,6 @@
1
1
  import contextlib
2
2
  from abc import ABC, abstractmethod
3
+ from datetime import datetime
3
4
  from typing import List, Optional, Type
4
5
  from zipfile import ZipFile
5
6
 
@@ -9,11 +10,13 @@ from cumulusci.core.config import OrgConfig
9
10
  from cumulusci.core.config.project_config import BaseProjectConfig
10
11
  from cumulusci.core.dependencies.utils import TaskContext
11
12
  from cumulusci.core.exceptions import DependencyResolutionError, VcsNotFoundError
13
+ from cumulusci.core.flowrunner import FlowCallback, FlowCoordinator
12
14
  from cumulusci.core.sfdx import (
13
15
  SourceFormat,
14
16
  convert_sfdx_source,
15
17
  get_source_format_for_zipfile,
16
18
  )
19
+ from cumulusci.core.utils import format_duration
17
20
  from cumulusci.salesforce_api.metadata import ApiDeploy
18
21
  from cumulusci.salesforce_api.package_zip import MetadataPackageZipBuilder
19
22
  from cumulusci.utils import download_extract_vcs_from_repo, temporary_dir
@@ -153,11 +156,8 @@ class DynamicDependency(Dependency, ABC):
153
156
  resolve_dependency(self, context, strategies)
154
157
 
155
158
 
156
- class UnmanagedDependency(StaticDependency, ABC):
157
- """Abstract base class for static, unmanaged dependencies."""
158
-
159
+ class UnmanagedStaticDependency(StaticDependency, ABC):
159
160
  unmanaged: Optional[bool] = None
160
- subfolder: Optional[str] = None
161
161
  namespace_inject: Optional[str] = None
162
162
  namespace_strip: Optional[str] = None
163
163
  collision_check: Optional[bool] = None
@@ -171,6 +171,12 @@ class UnmanagedDependency(StaticDependency, ABC):
171
171
 
172
172
  return self.unmanaged
173
173
 
174
+
175
+ class UnmanagedDependency(UnmanagedStaticDependency, ABC):
176
+ """Abstract base class for static, unmanaged dependencies."""
177
+
178
+ subfolder: Optional[str] = None
179
+
174
180
  @abstractmethod
175
181
  def _get_zip_src(self, context: BaseProjectConfig) -> ZipFile:
176
182
  pass
@@ -378,6 +384,28 @@ class VcsDynamicDependency(BaseVcsDynamicDependency, ABC):
378
384
 
379
385
  return values
380
386
 
387
+ def _flatten_dependency_flow(
388
+ self,
389
+ remote_config: BaseProjectConfig,
390
+ flow_type: str,
391
+ managed: bool,
392
+ namespace: Optional[str],
393
+ ) -> List[StaticDependency]:
394
+
395
+ if remote_config.project.get(flow_type):
396
+ return [
397
+ UnmanagedVcsDependencyFlow(
398
+ url=self.url,
399
+ vcs=self.vcs,
400
+ commit=self.ref,
401
+ flow_name=remote_config.project.get(flow_type),
402
+ unmanaged=not managed,
403
+ namespace_inject=namespace if namespace and managed else None,
404
+ namespace_strip=namespace if namespace and not managed else None,
405
+ )
406
+ ]
407
+ return []
408
+
381
409
  def _flatten_unpackaged(
382
410
  self,
383
411
  repo: AbstractRepo,
@@ -454,6 +482,17 @@ class VcsDynamicDependency(BaseVcsDynamicDependency, ABC):
454
482
  # Check for unmanaged flag on a namespaced package
455
483
  managed = bool(namespace and not self.unmanaged)
456
484
 
485
+ # Look for any flow to executed in project config
486
+ # Pre flows will run to dynamically generate metadata and deploy.
487
+ deps.extend(
488
+ self._flatten_dependency_flow(
489
+ package_config,
490
+ "dependency_flow_pre",
491
+ managed=managed,
492
+ namespace=namespace,
493
+ )
494
+ )
495
+
457
496
  # Look for subfolders under unpackaged/pre
458
497
  # unpackaged/pre is always deployed unmanaged, no namespace manipulation.
459
498
  deps.extend(
@@ -482,6 +521,17 @@ class VcsDynamicDependency(BaseVcsDynamicDependency, ABC):
482
521
  else:
483
522
  deps.append(self.package_dependency)
484
523
 
524
+ # Look for any flow to executed in project config
525
+ # Pre flows will run to dynamically generate metadata and deploy.
526
+ deps.extend(
527
+ self._flatten_dependency_flow(
528
+ package_config,
529
+ "dependency_flow_post",
530
+ managed=managed,
531
+ namespace=namespace,
532
+ )
533
+ )
534
+
485
535
  # We always inject the project's namespace into unpackaged/post metadata if managed
486
536
  deps.extend(
487
537
  self._flatten_unpackaged(
@@ -559,3 +609,73 @@ class UnmanagedVcsDependency(UnmanagedDependency, ABC):
559
609
  )
560
610
 
561
611
  return f"{self.url}{subfolder} @{self.ref}"
612
+
613
+
614
+ class UnmanagedVcsDependencyFlow(UnmanagedStaticDependency, ABC):
615
+ vcs: str
616
+ url: AnyUrl
617
+ commit: str
618
+ flow_name: str
619
+ callback_class = FlowCallback
620
+
621
+ # Add these fields to support namespace manipulation
622
+ namespace_inject: Optional[str] = None
623
+ namespace_strip: Optional[str] = None
624
+ password_env_name: Optional[str] = None
625
+
626
+ @property
627
+ def name(self):
628
+ return f"Deploy {self.url} Flow: {self.flow_name}"
629
+
630
+ @property
631
+ def description(self):
632
+ return f"{self.url} Flow: {self.flow_name} @{self.commit}"
633
+
634
+ def install(self, context: BaseProjectConfig, org: OrgConfig):
635
+ context.logger.info(f"Deploying dependency Flow from {self.description}")
636
+
637
+ from cumulusci.utils.yaml.cumulusci_yml import VCSSourceModel
638
+ from cumulusci.vcs.vcs_source import VCSSource
639
+
640
+ # Get the VCS Source class from the vcs field.
641
+ source_model = VCSSourceModel(
642
+ vcs=self.vcs,
643
+ url=self.url,
644
+ commit=self.commit,
645
+ allow_remote_code=context.allow_remote_code,
646
+ )
647
+ vcs_source = VCSSource.create(context, source_model)
648
+
649
+ # Fetch the data and get remote project config.
650
+ context.logger.info(f"Fetching from {vcs_source}")
651
+ project_config = vcs_source.fetch()
652
+
653
+ project_config.set_keychain(context.keychain)
654
+ project_config.source = vcs_source
655
+
656
+ # If I can't load remote code, make sure that my
657
+ # included repos can't either.
658
+ if vcs_source.allow_remote_code:
659
+ project_config._add_tasks_directory_to_python_path()
660
+
661
+ # Run the flow.
662
+ flow_config = project_config.get_flow(self.flow_name)
663
+ flow_config.name = self.flow_name
664
+
665
+ coordinator = FlowCoordinator(
666
+ project_config,
667
+ flow_config,
668
+ name=flow_config.name,
669
+ options={
670
+ "unmanaged": self._get_unmanaged(org),
671
+ "namespace_inject": self.namespace_inject,
672
+ "namespace_strip": self.namespace_strip,
673
+ },
674
+ skip=None,
675
+ callbacks=self.callback_class(),
676
+ )
677
+
678
+ start_time = datetime.now()
679
+ coordinator.run(org)
680
+ duration = datetime.now() - start_time
681
+ context.logger.info(f"Ran {self.flow_name} in {format_duration(duration)}")
@@ -9,7 +9,11 @@ from pydantic import ValidationError, root_validator
9
9
 
10
10
  from cumulusci.core.config.org_config import OrgConfig, VersionInfo
11
11
  from cumulusci.core.config.project_config import BaseProjectConfig
12
- from cumulusci.core.dependencies.base import DynamicDependency, StaticDependency
12
+ from cumulusci.core.dependencies.base import (
13
+ DynamicDependency,
14
+ StaticDependency,
15
+ UnmanagedVcsDependencyFlow,
16
+ )
13
17
  from cumulusci.core.dependencies.dependencies import (
14
18
  PackageNamespaceVersionDependency,
15
19
  PackageVersionIdDependency,
@@ -903,6 +907,203 @@ class TestUnmanagedZipURLDependency:
903
907
  zf.close()
904
908
 
905
909
 
910
+ # Concrete implementation of UnmanagedVcsDependencyFlow for testing
911
+ class ConcreteUnmanagedVcsDependencyFlow(UnmanagedVcsDependencyFlow):
912
+ """Concrete implementation of UnmanagedVcsDependencyFlow for testing"""
913
+
914
+ def __init__(self, **kwargs):
915
+ # Set default values for testing
916
+ defaults = {
917
+ "vcs": "github",
918
+ "url": "https://github.com/test/repo",
919
+ "commit": "abc123",
920
+ "flow_name": "test_flow",
921
+ }
922
+ defaults.update(kwargs)
923
+ super().__init__(**defaults)
924
+
925
+ @root_validator(pre=True)
926
+ def sync_vcs_and_url(cls, values):
927
+ """Implement required abstract method from base class"""
928
+ return _sync_vcs_and_url(values)
929
+
930
+
931
+ class TestUnmanagedVcsDependencyFlow:
932
+ def test_name(self):
933
+ """Test the name property"""
934
+ flow_dep = ConcreteUnmanagedVcsDependencyFlow(
935
+ url="https://github.com/test/repo", flow_name="install_deps"
936
+ )
937
+ expected_name = "Deploy https://github.com/test/repo Flow: install_deps"
938
+ assert flow_dep.name == expected_name
939
+
940
+ def test_description(self):
941
+ """Test the description property"""
942
+ flow_dep = ConcreteUnmanagedVcsDependencyFlow(
943
+ url="https://github.com/test/repo",
944
+ flow_name="install_deps",
945
+ commit="abc123",
946
+ )
947
+ expected_description = "https://github.com/test/repo Flow: install_deps @abc123"
948
+ assert flow_dep.description == expected_description
949
+
950
+ def test_get_unmanaged(self):
951
+ """Test the _get_unmanaged method inherited from UnmanagedStaticDependency"""
952
+ # Test with unmanaged = None and no namespace_inject
953
+ flow_dep = ConcreteUnmanagedVcsDependencyFlow(
954
+ unmanaged=None, namespace_inject=None
955
+ )
956
+ org_config = mock.Mock()
957
+ org_config.installed_packages = {}
958
+ assert flow_dep._get_unmanaged(org_config) is True
959
+
960
+ # Test with unmanaged = None and namespace_inject present but not in installed packages
961
+ flow_dep = ConcreteUnmanagedVcsDependencyFlow(
962
+ unmanaged=None, namespace_inject="test_ns"
963
+ )
964
+ org_config = mock.Mock()
965
+ org_config.installed_packages = {}
966
+ assert flow_dep._get_unmanaged(org_config) is True
967
+
968
+ # Test with unmanaged = None and namespace_inject present and in installed packages
969
+ flow_dep = ConcreteUnmanagedVcsDependencyFlow(
970
+ unmanaged=None, namespace_inject="test_ns"
971
+ )
972
+ org_config = mock.Mock()
973
+ org_config.installed_packages = {"test_ns": mock.Mock()}
974
+ assert flow_dep._get_unmanaged(org_config) is False
975
+
976
+ # Test with explicit unmanaged = True
977
+ flow_dep = ConcreteUnmanagedVcsDependencyFlow(unmanaged=True)
978
+ org_config = mock.Mock()
979
+ assert flow_dep._get_unmanaged(org_config) is True
980
+
981
+ # Test with explicit unmanaged = False
982
+ flow_dep = ConcreteUnmanagedVcsDependencyFlow(unmanaged=False)
983
+ org_config = mock.Mock()
984
+ assert flow_dep._get_unmanaged(org_config) is False
985
+
986
+ @mock.patch("cumulusci.core.dependencies.base.datetime")
987
+ @mock.patch("cumulusci.core.dependencies.base.format_duration")
988
+ @mock.patch("cumulusci.core.dependencies.base.FlowCoordinator")
989
+ @mock.patch("cumulusci.vcs.vcs_source.VCSSource")
990
+ @mock.patch("cumulusci.utils.yaml.cumulusci_yml.VCSSourceModel")
991
+ def test_install(
992
+ self,
993
+ vcs_source_model_mock,
994
+ vcs_source_mock,
995
+ flow_coordinator_mock,
996
+ format_duration_mock,
997
+ datetime_mock,
998
+ ):
999
+ """Test the install method"""
1000
+ # Setup mocks
1001
+ mock_context = mock.Mock()
1002
+ mock_context.logger = mock.Mock()
1003
+ mock_context.allow_remote_code = False
1004
+ mock_context.keychain = mock.Mock()
1005
+
1006
+ mock_org = mock.Mock()
1007
+ mock_org.installed_packages = {}
1008
+
1009
+ # Setup VCS source mock
1010
+ mock_vcs_source_instance = mock.Mock()
1011
+ mock_vcs_source_instance.allow_remote_code = False
1012
+ vcs_source_mock.create.return_value = mock_vcs_source_instance
1013
+
1014
+ # Setup project config mock
1015
+ mock_project_config = mock.Mock()
1016
+ mock_vcs_source_instance.fetch.return_value = mock_project_config
1017
+
1018
+ # Setup flow config mock
1019
+ mock_flow_config = mock.Mock()
1020
+ mock_project_config.get_flow.return_value = mock_flow_config
1021
+
1022
+ # Setup coordinator mock
1023
+ mock_coordinator_instance = mock.Mock()
1024
+ flow_coordinator_mock.return_value = mock_coordinator_instance
1025
+
1026
+ # Setup datetime mock
1027
+ from datetime import datetime as real_datetime
1028
+
1029
+ mock_start_time = real_datetime(2023, 1, 1, 10, 0, 0)
1030
+ mock_end_time = real_datetime(2023, 1, 1, 10, 0, 5)
1031
+ datetime_mock.now.side_effect = [mock_start_time, mock_end_time]
1032
+
1033
+ # Setup format duration mock
1034
+ format_duration_mock.return_value = "5.2s"
1035
+
1036
+ # Create flow dependency
1037
+ flow_dep = ConcreteUnmanagedVcsDependencyFlow(
1038
+ vcs="github",
1039
+ url="https://github.com/test/repo",
1040
+ commit="abc123",
1041
+ flow_name="install_deps",
1042
+ namespace_inject="test_ns",
1043
+ namespace_strip="old_ns",
1044
+ )
1045
+
1046
+ # Call install
1047
+ flow_dep.install(mock_context, mock_org)
1048
+
1049
+ # Verify VCSSourceModel was created correctly
1050
+ vcs_source_model_mock.assert_called_once_with(
1051
+ vcs="github",
1052
+ url="https://github.com/test/repo",
1053
+ commit="abc123",
1054
+ allow_remote_code=False,
1055
+ )
1056
+
1057
+ # Verify VCSSource.create was called
1058
+ vcs_source_mock.create.assert_called_once_with(
1059
+ mock_context, vcs_source_model_mock.return_value
1060
+ )
1061
+
1062
+ # Verify fetch was called
1063
+ mock_vcs_source_instance.fetch.assert_called_once()
1064
+
1065
+ # Verify project config setup
1066
+ mock_project_config.set_keychain.assert_called_once_with(mock_context.keychain)
1067
+ assert mock_project_config.source == mock_vcs_source_instance
1068
+
1069
+ # Verify flow config setup
1070
+ mock_project_config.get_flow.assert_called_once_with("install_deps")
1071
+ assert mock_flow_config.name == "install_deps"
1072
+
1073
+ # Verify FlowCoordinator was created correctly
1074
+ flow_coordinator_mock.assert_called_once()
1075
+ args, kwargs = flow_coordinator_mock.call_args
1076
+ assert args[0] == mock_project_config
1077
+ assert args[1] == mock_flow_config
1078
+ 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
+ }
1084
+ assert kwargs["skip"] is None
1085
+ assert isinstance(kwargs["callbacks"], type(flow_dep.callback_class()))
1086
+
1087
+ # Verify coordinator.run was called
1088
+ mock_coordinator_instance.run.assert_called_once_with(mock_org)
1089
+
1090
+ # Verify logging
1091
+ assert (
1092
+ mock_context.logger.info.call_count == 3
1093
+ ) # Initial log, fetching log, final log
1094
+ mock_context.logger.info.assert_any_call(
1095
+ "Deploying dependency Flow from https://github.com/test/repo Flow: install_deps @abc123"
1096
+ )
1097
+ mock_context.logger.info.assert_any_call(
1098
+ f"Fetching from {mock_vcs_source_instance}"
1099
+ )
1100
+ mock_context.logger.info.assert_any_call("Ran install_deps in 5.2s")
1101
+
1102
+ # Verify datetime and format_duration calls
1103
+ assert datetime_mock.now.call_count == 2
1104
+ format_duration_mock.assert_called_once_with(mock_end_time - mock_start_time)
1105
+
1106
+
906
1107
  class TestParseDependency:
907
1108
  def test_parse_managed_package_dep(self):
908
1109
  m = parse_dependency({"version": "1.0", "namespace": "foo"})
@@ -139,3 +139,154 @@ class TestProcessListOfPairsDictArg:
139
139
  error_message = re.escape("Var specified twice: foo")
140
140
  with pytest.raises(TaskOptionsError, match=error_message):
141
141
  utils.process_list_of_pairs_dict_arg(duplicate)
142
+
143
+
144
+ class TestDeepMergePlugins:
145
+ """Test the deep_merge_plugins function"""
146
+
147
+ def test_deep_merge_plugins_remote_takes_precedence(self):
148
+ """Test that remote plugins take precedence over project plugins"""
149
+ remote_plugins = {"plugin1": {"setting": "remote_value"}}
150
+ project_plugins = {"plugin1": {"setting": "project_value"}}
151
+
152
+ result = utils.deep_merge_plugins(remote_plugins, project_plugins)
153
+
154
+ assert result == {"plugin1": {"setting": "remote_value"}}
155
+
156
+ def test_deep_merge_plugins_project_provides_defaults(self):
157
+ """Test that project plugins provide defaults for missing keys"""
158
+ remote_plugins = {"plugin1": {"setting1": "remote_value"}}
159
+ project_plugins = {"plugin1": {"setting2": "project_value"}}
160
+
161
+ result = utils.deep_merge_plugins(remote_plugins, project_plugins)
162
+
163
+ assert result == {
164
+ "plugin1": {"setting1": "remote_value", "setting2": "project_value"}
165
+ }
166
+
167
+ def test_deep_merge_plugins_missing_keys_from_project(self):
168
+ """Test that missing top-level keys are added from project plugins"""
169
+ remote_plugins = {"plugin1": {"setting": "remote_value"}}
170
+ project_plugins = {"plugin2": {"setting": "project_value"}}
171
+
172
+ result = utils.deep_merge_plugins(remote_plugins, project_plugins)
173
+
174
+ assert result == {
175
+ "plugin1": {"setting": "remote_value"},
176
+ "plugin2": {"setting": "project_value"},
177
+ }
178
+
179
+ def test_deep_merge_plugins_recursive_merge(self):
180
+ """Test recursive merging of nested dictionaries"""
181
+ remote_plugins = {
182
+ "plugin1": {
183
+ "nested": {"setting1": "remote_value", "setting2": "remote_value2"}
184
+ }
185
+ }
186
+ project_plugins = {
187
+ "plugin1": {
188
+ "nested": {
189
+ "setting2": "project_value2", # Should be overridden
190
+ "setting3": "project_value3", # Should be added
191
+ }
192
+ }
193
+ }
194
+
195
+ result = utils.deep_merge_plugins(remote_plugins, project_plugins)
196
+
197
+ assert result == {
198
+ "plugin1": {
199
+ "nested": {
200
+ "setting1": "remote_value",
201
+ "setting2": "remote_value2",
202
+ "setting3": "project_value3",
203
+ }
204
+ }
205
+ }
206
+
207
+ def test_deep_merge_plugins_non_dict_inputs(self):
208
+ """Test that non-dict inputs return the remote plugins unchanged"""
209
+ remote_plugins = {"plugin1": {"setting": "value"}}
210
+
211
+ # Test with non-dict project_plugins
212
+ result = utils.deep_merge_plugins(remote_plugins, "not_a_dict")
213
+ assert result == remote_plugins
214
+
215
+ # Test with non-dict remote_plugins
216
+ result = utils.deep_merge_plugins(
217
+ "not_a_dict", {"plugin1": {"setting": "value"}}
218
+ )
219
+ assert result == "not_a_dict"
220
+
221
+ # Test with both non-dict
222
+ result = utils.deep_merge_plugins("remote", "project")
223
+ assert result == "remote"
224
+
225
+ def test_deep_merge_plugins_type_mismatch(self):
226
+ """Test that type mismatches preserve remote values"""
227
+ remote_plugins = {"plugin1": {"setting": "string_value"}}
228
+ project_plugins = {"plugin1": {"setting": {"nested": "dict_value"}}}
229
+
230
+ result = utils.deep_merge_plugins(remote_plugins, project_plugins)
231
+
232
+ # Remote takes precedence when types don't match
233
+ assert result == {"plugin1": {"setting": "string_value"}}
234
+
235
+ def test_deep_merge_plugins_deep_copy(self):
236
+ """Test that deep copy is used to avoid modifying original data"""
237
+ remote_plugins = {"plugin1": {"setting": "remote_value"}}
238
+ project_plugins = {"plugin2": {"nested": {"setting": "project_value"}}}
239
+
240
+ result = utils.deep_merge_plugins(remote_plugins, project_plugins)
241
+
242
+ # Modify the result to check if originals are affected
243
+ result["plugin2"]["nested"]["setting"] = "modified_value"
244
+
245
+ # Original should remain unchanged
246
+ assert project_plugins["plugin2"]["nested"]["setting"] == "project_value"
247
+
248
+ def test_deep_merge_plugins_empty_inputs(self):
249
+ """Test with empty dictionaries"""
250
+ # Empty remote, non-empty project
251
+ result = utils.deep_merge_plugins({}, {"plugin1": {"setting": "value"}})
252
+ assert result == {"plugin1": {"setting": "value"}}
253
+
254
+ # Non-empty remote, empty project
255
+ result = utils.deep_merge_plugins({"plugin1": {"setting": "value"}}, {})
256
+ assert result == {"plugin1": {"setting": "value"}}
257
+
258
+ # Both empty
259
+ result = utils.deep_merge_plugins({}, {})
260
+ assert result == {}
261
+
262
+ def test_deep_merge_plugins_complex_nested_structure(self):
263
+ """Test with complex nested structure"""
264
+ remote_plugins = {
265
+ "plugin1": {"level1": {"level2": {"remote_setting": "remote_value"}}}
266
+ }
267
+ project_plugins = {
268
+ "plugin1": {
269
+ "level1": {
270
+ "level2": {"project_setting": "project_value"},
271
+ "project_level2": "project_value2",
272
+ },
273
+ "project_level1": "project_value3",
274
+ }
275
+ }
276
+
277
+ result = utils.deep_merge_plugins(remote_plugins, project_plugins)
278
+
279
+ expected = {
280
+ "plugin1": {
281
+ "level1": {
282
+ "level2": {
283
+ "remote_setting": "remote_value",
284
+ "project_setting": "project_value",
285
+ },
286
+ "project_level2": "project_value2",
287
+ },
288
+ "project_level1": "project_value3",
289
+ }
290
+ }
291
+
292
+ assert result == expected
cumulusci/core/utils.py CHANGED
@@ -400,3 +400,26 @@ def determine_managed_mode(options, project_config, org_config):
400
400
  else:
401
401
  # Fall back to checking namespace in installed packages
402
402
  return bool(namespace) and namespace in installed_packages
403
+
404
+
405
+ def deep_merge_plugins(remote_plugins, project_plugins):
406
+ """
407
+ Deep merge project_plugins into remote_plugins, adding only missing keys.
408
+ Remote plugins take precedence, project plugins provide defaults for missing keys.
409
+ """
410
+ if not isinstance(remote_plugins, dict) or not isinstance(project_plugins, dict):
411
+ return remote_plugins
412
+
413
+ result = remote_plugins.copy()
414
+
415
+ for key, value in project_plugins.items():
416
+ if key not in result:
417
+ # Key doesn't exist in remote, add it from project
418
+ result[key] = copy.deepcopy(value)
419
+ elif isinstance(result[key], dict) and isinstance(value, dict):
420
+ # Both are dictionaries, recursively merge
421
+ result[key] = deep_merge_plugins(result[key], value)
422
+ # If key exists in remote but types don't match or remote value is not dict,
423
+ # keep the remote value (remote takes precedence)
424
+
425
+ return result
@@ -426,6 +426,14 @@
426
426
  "custom": {
427
427
  "title": "Custom",
428
428
  "type": "object"
429
+ },
430
+ "dependency_flow_pre": {
431
+ "title": "Dependency Flow Pre",
432
+ "type": "string"
433
+ },
434
+ "dependency_flow_post": {
435
+ "title": "Dependency Flow Post",
436
+ "type": "string"
429
437
  }
430
438
  },
431
439
  "additionalProperties": false
@@ -144,6 +144,8 @@ class Project(CCIDictModel):
144
144
  dependency_pins: Optional[List[Dict[str, str]]]
145
145
  source_format: Literal["sfdx", "mdapi"] = "mdapi"
146
146
  custom: Optional[Dict] = None
147
+ dependency_flow_pre: str = None
148
+ dependency_flow_post: str = None
147
149
 
148
150
 
149
151
  class ScratchOrg(CCIDictModel):
@@ -255,6 +255,7 @@ class VCSSource(ABC):
255
255
  "name": self.repo.repo_name,
256
256
  "url": self.url,
257
257
  "commit": self.commit,
258
+ "vcs_service": self.vcs_service,
258
259
  # Note: we currently only pass the branch if it was explicitly
259
260
  # included in the source spec. If the commit was found another way,
260
261
  # we aren't looking up what branches have that commit as their head.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cumulusci-plus
3
- Version: 5.0.5
3
+ Version: 5.0.8
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
@@ -1,10 +1,10 @@
1
- cumulusci/__about__.py,sha256=uRjqoB5BCxf3yRehPmqmoRFXGMu-mthc51Cx8UK5oRw,22
1
+ cumulusci/__about__.py,sha256=iknMV3i3Ktpb-eCRBUG-Q--oWKy45MyfhUxfGmOFozU,22
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
5
  cumulusci/cumulusci.yml,sha256=FE1Dm7EhcMXY-XtLljbcbRyo_E0tvspaxqh8Nsiup3w,72365
6
6
  cumulusci/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- cumulusci/cli/cci.py,sha256=GQKxxkz1furfcaDlm7qc2P4cww0GvhlNr2dpc_Hp9Lw,8523
7
+ cumulusci/cli/cci.py,sha256=fDA3Kym5OoKOsjW4YSPxCLnGIu9y7NrCiIZHvial2ck,11513
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
@@ -18,7 +18,7 @@ cumulusci/cli/task.py,sha256=xm8lo0_LMMpcsUDv1Gj_HpW1phllyEW9IRm2lQSh5wg,10077
18
18
  cumulusci/cli/ui.py,sha256=Ld-2S6Kr204SBput-1pNAVlYglzcvbV5nVA_rGXlAo8,7346
19
19
  cumulusci/cli/utils.py,sha256=Bl-l8eOXxzi6E_DsWGGGeefwZxrVg7Zo52BIwoNhKH8,5522
20
20
  cumulusci/cli/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- cumulusci/cli/tests/test_cci.py,sha256=wtzuXggLGrbMWjMQMVkdqXnYT_rqAr68xWCjTjbhH4w,18142
21
+ cumulusci/cli/tests/test_cci.py,sha256=oosAcnp3CCkRSs_izsT8zPTXoDUdbPO-_pfCoaTACGk,26917
22
22
  cumulusci/cli/tests/test_error.py,sha256=zU2ccfGOivcVMpCManam18uyhlzT-HFb9xuMljU4few,6629
23
23
  cumulusci/cli/tests/test_flow.py,sha256=CIkZWvai5H4EOs35epMKWmxzDfauIEqL6BUwC8q3LPU,8535
24
24
  cumulusci/cli/tests/test_logger.py,sha256=-XBwSmtuelpx5sax0_E2xi4F9m_UsRkt2gn1o9ub6h8,834
@@ -43,7 +43,7 @@ cumulusci/core/runtime.py,sha256=H2e3YN2i_nPiB2oVB_0V0rLsmTqBs8CdajdGOA4gsB0,377
43
43
  cumulusci/core/sfdx.py,sha256=ZGW71iMdcMa8RlgZFXcrzZJg5ADCRtzZ-_P8DVUvYJg,4788
44
44
  cumulusci/core/tasks.py,sha256=FF96ywx9VF1nSWQXyPrfFXcPIdsTkA9dvgxaKlKmkg8,14142
45
45
  cumulusci/core/template_utils.py,sha256=to1kv-4gaKELNYIpeAUSHcJOvwgRx1sbI5wdz_I0Cnc,1409
46
- cumulusci/core/utils.py,sha256=S7shYnPaUd54WpmArVR2jyGcjJiRh6y6y_csr-lqJJI,14593
46
+ cumulusci/core/utils.py,sha256=7E2HI-FEy7K-PsHHejISI6VDKuhW9QkfJ97RTg-me4I,15527
47
47
  cumulusci/core/versions.py,sha256=TooqfsKAdEomLsjQaYvarPDu34kNTPeIvEzXLhuUev4,4675
48
48
  cumulusci/core/config/BaseConfig.py,sha256=WFz6jq2xnUHyi-Plm3ARpqAZtDlI4zLact3e5gqO-Yc,130
49
49
  cumulusci/core/config/BaseTaskFlowConfig.py,sha256=vdUijsUtURFl9e3M8qv-KSLkLBSsMFh-zuDgbPcyDKA,158
@@ -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=Lf5yKZaEUYabFMyrmU-4L04N1RU1-2sakbwhHRFxlv8,18770
68
+ cumulusci/core/dependencies/base.py,sha256=i5F7D2CwoWwUHqjI0LRCwwJGqhGKFRDkerg0N8uJdQs,22804
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=V7clNtNN8n2jCw45BzO7clZE4YyCB-qN-KI_GdlucGE,33024
76
+ cumulusci/core/dependencies/tests/test_dependencies.py,sha256=MiKet_2xCMhzyo3oz_DzBnwVBjfCetmbmpBVQVKMEpg,40705
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
@@ -103,7 +103,7 @@ cumulusci/core/tests/test_github.py,sha256=cXsCAMzub2EUAI0_x2CYBR-bn4FE_OYe72zZO
103
103
  cumulusci/core/tests/test_sfdx.py,sha256=-pC6oGVF5H_OQfZxUG_YbPCYddxWhBS6gPImKO-XqN4,4540
104
104
  cumulusci/core/tests/test_source.py,sha256=m66vXcgMwc92R1OotTdMzd6Mg6gAHj5QrYJMDnL_i5s,24468
105
105
  cumulusci/core/tests/test_tasks.py,sha256=hJbVmecbJekOJQdB67aMmPSirPCCf-MGGAnfkLFdTfs,9543
106
- cumulusci/core/tests/test_utils.py,sha256=TpNzuX_WrRKuKyDn9gJd3FFzP_-kxavFAd9mBgsu8EI,5116
106
+ cumulusci/core/tests/test_utils.py,sha256=rDvBePhwyrrMOlSXArzmSwO7S2jyny1y23vIi7w2AX0,10810
107
107
  cumulusci/core/tests/test_utils_merge_config.py,sha256=FfezjbsW7-45T9Apf6DqUXdxMyu7CtlG3_8CsUghqJ0,9676
108
108
  cumulusci/core/tests/test_versions.py,sha256=s7JsmBAHN8FA2KWRFzVxGj-EgyA05hiFGnfRvkGgLtM,2427
109
109
  cumulusci/core/tests/utils.py,sha256=odciQ496J-LxTrKOXGyNx-yaprbUL713eZlh6h9W49g,3182
@@ -224,7 +224,7 @@ cumulusci/salesforce_api/tests/test_package_zip.py,sha256=C8YzRFvRRpgT1ysqUMGhGV
224
224
  cumulusci/salesforce_api/tests/test_rest_deploy.py,sha256=VRADL5vHlCJcswLk1IA6Chs6UD6YJUzPiM3KzPJ63L8,10045
225
225
  cumulusci/salesforce_api/tests/test_retrieve_profile_api.py,sha256=oDE0fM992y0N7fI94oJtRXJzIQ5sew285-ywDk4c-S4,11778
226
226
  cumulusci/salesforce_api/tests/test_utils.py,sha256=h3dGnr-au7H9QUzUv-3uDFEjP6eq29MXFQeYi3Cxfwk,3965
227
- cumulusci/schema/cumulusci.jsonschema.json,sha256=16BHBrlplqvuL3Fy-M2MCDLxD07Oc-OfUudAzm5umOA,24682
227
+ cumulusci/schema/cumulusci.jsonschema.json,sha256=V3VGXNslrrqGbceSmjvmnkG99b2GrwhXhYugz2FGPWw,24982
228
228
  cumulusci/tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
229
229
  cumulusci/tasks/base_source_control_task.py,sha256=YVLngLcXsjnsbQwTS33tioLic3EU5D-P0GtY8op00Uw,637
230
230
  cumulusci/tasks/command.py,sha256=ntVmi98kUvoQm90sd34pbILLt73tdG9VUT1-eTdHQ6g,5710
@@ -711,7 +711,7 @@ cumulusci/utils/xml/robot_xml.py,sha256=V9daywDdxBMDUYvMchWaPx-rj9kCXYskua1hvG_t
711
711
  cumulusci/utils/xml/salesforce_encoding.py,sha256=ahPJWYfq3NMW8oSjsrzzQnAqghL-nQBWn-lSXuGjhIc,3071
712
712
  cumulusci/utils/xml/test/test_metadata_tree.py,sha256=4jzKqYUEVzK6H8iCWMTD2v2hRssz2OeLspQO9osqm2E,8517
713
713
  cumulusci/utils/xml/test/test_salesforce_encoding.py,sha256=DN0z5sM6Lk1U2hD7nYt7eYK1BjJy3-d5OdHk4HsPo6Y,6309
714
- cumulusci/utils/yaml/cumulusci_yml.py,sha256=cWk2o7UzyX4JsDQIAEprglKqNl-iC78_QEQA4yjjJgY,11279
714
+ cumulusci/utils/yaml/cumulusci_yml.py,sha256=OmoxbE9jczQ1w9UfwJefROdNlChHwXsCJm9e6Zw3NJM,11352
715
715
  cumulusci/utils/yaml/model_parser.py,sha256=aTnlWwfMpDOGnlVKLvPAl5oLH0FReVeqDM-EUA4s7k8,5057
716
716
  cumulusci/utils/yaml/safer_loader.py,sha256=O7hhI2DqVA0GV4oY4rlB084QCc_eXFg2HTNHlfGVOcQ,2388
717
717
  cumulusci/utils/yaml/tests/bad_cci.yml,sha256=Idj6subt7pNZk8A1Vt3bs_lM99Ash5ZVgOho1drMTXQ,17
@@ -722,7 +722,7 @@ cumulusci/utils/yaml/tests/cassettes/TestCumulusciYml.test_validate_url__with_er
722
722
  cumulusci/vcs/base.py,sha256=HxkCaVf0-Oj7L9y9x0k3fb9RCQb9GVHpvedTPnYXpOM,6460
723
723
  cumulusci/vcs/bootstrap.py,sha256=vhvYFep6wu74CSbR_M9HMHMPBLbhih6rqgZnN_z-zHI,9266
724
724
  cumulusci/vcs/models.py,sha256=dSxQFZj_620tXO7JPoGgxgSNLYUup3lnqnrHxP0rNqA,22976
725
- cumulusci/vcs/vcs_source.py,sha256=zDciXg4aqVZ3vrFgHyiElNUabeb_T9f2baq75j2jrHM,10445
725
+ cumulusci/vcs/vcs_source.py,sha256=0_kWOSxNUhVxBOgmY7WiEKSbwwZhx_jcfVs5lTobE4E,10494
726
726
  cumulusci/vcs/github/__init__.py,sha256=LUnwWubhqDLXiej9AZvC9WryXiMhsnbYD8CiJgbJ0Ek,512
727
727
  cumulusci/vcs/github/adapter.py,sha256=DN2std4UgNW_eeuvI4fuWxYhzj4pAUYAiTYJvVh9v4A,23769
728
728
  cumulusci/vcs/github/service.py,sha256=Mbb6y6ngHLBR2J24Awn02I_rC1ufjdiYn3gGv0si5cQ,21265
@@ -736,9 +736,9 @@ cumulusci/vcs/tests/dummy_service.py,sha256=RltOUpMIhSDNrfxk0LhLqlH4ppC0sK6NC2cO
736
736
  cumulusci/vcs/tests/test_vcs_base.py,sha256=9mp6uZ3lTxY4onjUNCucp9N9aB3UylKS7_2Zu_hdAZw,24331
737
737
  cumulusci/vcs/tests/test_vcs_bootstrap.py,sha256=N0NA48-rGNIIjY3Z7PtVnNwHObSlEGDk2K55TQGI8g4,27954
738
738
  cumulusci/vcs/utils/__init__.py,sha256=py4fEcHM7Vd0M0XWznOlywxaeCtG3nEVGmELmEKVGU8,869
739
- cumulusci_plus-5.0.5.dist-info/METADATA,sha256=YGwFjRWNfqqoJO0Bd_rMYvXyVtQ7m1IU0zxPiMjv2G0,6134
740
- cumulusci_plus-5.0.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
741
- cumulusci_plus-5.0.5.dist-info/entry_points.txt,sha256=nTtu04b9iLXhzADcTrb5PwmdXE6e2MTUAMh9OK6Z2pg,80
742
- cumulusci_plus-5.0.5.dist-info/licenses/AUTHORS.rst,sha256=PvewjKImdKPhhJ6xR2EEZ4T7GbpY2ZeAeyWm2aLtiMQ,676
743
- cumulusci_plus-5.0.5.dist-info/licenses/LICENSE,sha256=NFsF_s7RVXk2dU6tmRAN8wF45pnD98VZ5IwqOsyBcaU,1499
744
- cumulusci_plus-5.0.5.dist-info/RECORD,,
739
+ cumulusci_plus-5.0.8.dist-info/METADATA,sha256=8u-lOMmYllxq6LLwXp7uUzNerM3gi0BDn4lTDExZ_eA,6134
740
+ cumulusci_plus-5.0.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
741
+ cumulusci_plus-5.0.8.dist-info/entry_points.txt,sha256=nTtu04b9iLXhzADcTrb5PwmdXE6e2MTUAMh9OK6Z2pg,80
742
+ cumulusci_plus-5.0.8.dist-info/licenses/AUTHORS.rst,sha256=PvewjKImdKPhhJ6xR2EEZ4T7GbpY2ZeAeyWm2aLtiMQ,676
743
+ cumulusci_plus-5.0.8.dist-info/licenses/LICENSE,sha256=NFsF_s7RVXk2dU6tmRAN8wF45pnD98VZ5IwqOsyBcaU,1499
744
+ cumulusci_plus-5.0.8.dist-info/RECORD,,