cumulusci-plus 5.0.6__py3-none-any.whl → 5.0.8.dev0__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.6"
1
+ __version__ = "5.0.8.dev0"
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,78 @@ 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
+ # Only use process group termination on Unix systems
97
+ if hasattr(os, "getpgrp") and hasattr(os, "killpg"):
98
+ pgrp = os.getpgrp()
99
+ # Send signal to all processes in the group except ourselves
100
+ os.killpg(pgrp, signum)
101
+ else:
102
+ # On Windows, we can't use process groups, so just log the attempt
103
+ console.print(
104
+ "[yellow]Process group termination not supported on this platform[/yellow]"
105
+ )
106
+ finally:
107
+ # Restore the original signal handler
108
+ signal.signal(signum, old_handler)
109
+
110
+ except ProcessLookupError:
111
+ # Process group may not exist or may already be terminated
112
+ pass
113
+ except Exception as e:
114
+ console.print(f"[red]Warning: Error terminating child processes: {e}[/red]")
115
+
116
+ # Exit with appropriate failure code
117
+ exit_code = 143 if signum == signal.SIGTERM else 130 # Standard exit codes
118
+ sys.exit(exit_code)
119
+
46
120
 
47
121
  #
48
122
  # Root command
@@ -54,7 +128,25 @@ def main(args=None):
54
128
 
55
129
  This wraps the `click` library in order to do some initialization and centralized error handling.
56
130
  """
131
+ global _exit_stack
132
+
133
+ # Create a new process group so we can terminate all child processes
134
+ # when we receive a termination signal
135
+ try:
136
+ if hasattr(os, "setpgrp"):
137
+ # On Unix systems, create a new process group
138
+ os.setpgrp()
139
+ except Exception:
140
+ # On Windows or if setpgrp fails, continue without process group
141
+ pass
142
+
143
+ # Set up signal handlers for graceful termination
144
+ signal.signal(signal.SIGTERM, _signal_handler)
145
+ signal.signal(signal.SIGINT, _signal_handler)
146
+
57
147
  with contextlib.ExitStack() as stack:
148
+ _exit_stack = stack # Store reference for signal handler cleanup
149
+
58
150
  args = args or sys.argv
59
151
 
60
152
  # (If enabled) set up requests to validate certs using system CA certs instead of certifi
@@ -98,15 +190,20 @@ def main(args=None):
98
190
  sys.exit(1)
99
191
  except Exception as e:
100
192
  if debug:
193
+ console = Console()
101
194
  show_debug_info()
195
+ console.print(
196
+ f"\n[red bold]Debug info for bug reports:\n{traceback.format_exc()}"
197
+ )
198
+ sys.exit(1)
102
199
  else:
103
200
  handle_exception(
104
- e,
105
- is_error_command,
106
- tempfile_path,
107
- should_show_stacktraces,
201
+ e, is_error_command, tempfile_path, should_show_stacktraces
108
202
  )
109
- sys.exit(1)
203
+ sys.exit(1)
204
+
205
+ # Clear the global reference when exiting normally
206
+ _exit_stack = None
110
207
 
111
208
 
112
209
  def handle_exception(
@@ -1,9 +1,8 @@
1
1
  import contextlib
2
2
  import io
3
3
  import os
4
- import shutil
4
+ import signal
5
5
  import sys
6
- import tempfile
7
6
  from pathlib import Path
8
7
  from unittest import mock
9
8
 
@@ -26,22 +25,11 @@ CONSOLE = mock.Mock()
26
25
 
27
26
 
28
27
  @pytest.fixture(autouse=True)
29
- def env_config():
30
- config = {
31
- "global_tempdir": tempfile.gettempdir(),
32
- "tempdir": tempfile.mkdtemp(),
33
- "environ_mock": mock.patch.dict(
34
- os.environ, {"HOME": tempfile.mkdtemp(), "CUMULUSCI_KEY": ""}
35
- ),
36
- }
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"])
28
+ def reset_signal_handler_flag():
29
+ """Reset signal handler flag to ensure clean state for tests"""
30
+ cci._signal_handler_active = False
31
+ yield
32
+ cci._signal_handler_active = False
45
33
 
46
34
 
47
35
  @mock.patch("cumulusci.cli.cci.tee_stdout_stderr")
@@ -266,7 +254,9 @@ def test_cci_org_default__no_orgname(
266
254
  DEPLOY_CLASS_PATH = f"cumulusci.tasks.salesforce.Deploy{'.Deploy' if sys.version_info >= (3, 11) else ''}"
267
255
 
268
256
 
269
- @mock.patch("cumulusci.cli.cci.init_logger", mock.Mock())
257
+ @mock.patch(
258
+ "cumulusci.cli.cci.init_logger", mock.Mock()
259
+ ) # side effects break other tests
270
260
  @mock.patch("cumulusci.cli.cci.tee_stdout_stderr", mock.MagicMock())
271
261
  @mock.patch(f"{DEPLOY_CLASS_PATH}.__call__", mock.Mock())
272
262
  @mock.patch("sys.exit", mock.Mock())
@@ -543,3 +533,264 @@ def test_dash_dash_version(
543
533
 
544
534
  cci.main(["cci", "--version"])
545
535
  assert len(show_version_info.mock_calls) == 2
536
+
537
+
538
+ @mock.patch("sys.exit")
539
+ @mock.patch("cumulusci.cli.cci.Console")
540
+ @mock.patch("os.killpg", create=True)
541
+ @mock.patch("os.getpgrp", create=True)
542
+ @mock.patch("signal.signal")
543
+ def test_signal_handler_terminates_process_group(
544
+ mock_signal, mock_getpgrp, mock_killpg, mock_console, mock_exit
545
+ ):
546
+ """Test that the signal handler terminates the process group"""
547
+ console_instance = mock_console.return_value
548
+ mock_getpgrp.return_value = 1234 # Mock process group ID
549
+
550
+ # Mock the global exit stack
551
+ mock_exit_stack = mock.Mock()
552
+ with mock.patch.object(cci, "_exit_stack", mock_exit_stack):
553
+ # Mock hasattr to return True for Unix functions
554
+ with mock.patch("builtins.hasattr", return_value=True):
555
+ # Call the signal handler with SIGTERM
556
+ cci._signal_handler(signal.SIGTERM, None)
557
+
558
+ # Verify console output
559
+ console_instance.print.assert_any_call(
560
+ "\n[yellow]Received SIGTERM - CumulusCI is being terminated[/yellow]"
561
+ )
562
+ console_instance.print.assert_any_call(
563
+ "[yellow]Exiting with failure code due to external cancellation.[/yellow]"
564
+ )
565
+ console_instance.print.assert_any_call(
566
+ "[yellow]Terminating child processes...[/yellow]"
567
+ )
568
+
569
+ # Verify cleanup was called
570
+ mock_exit_stack.close.assert_called_once()
571
+
572
+ # Verify signal was temporarily ignored and then restored
573
+ mock_signal.assert_called()
574
+
575
+ # Verify process group was terminated
576
+ mock_getpgrp.assert_called_once()
577
+ mock_killpg.assert_called_once_with(1234, signal.SIGTERM)
578
+
579
+ # Verify exit with correct code
580
+ mock_exit.assert_called_once_with(143)
581
+
582
+
583
+ @mock.patch("sys.exit")
584
+ @mock.patch("cumulusci.cli.cci.Console")
585
+ @mock.patch("os.killpg", create=True)
586
+ @mock.patch("os.getpgrp", create=True)
587
+ @mock.patch("signal.signal")
588
+ def test_signal_handler_sigint(
589
+ mock_signal, mock_getpgrp, mock_killpg, mock_console, mock_exit
590
+ ):
591
+ """Test that the signal handler properly handles SIGINT"""
592
+ console_instance = mock_console.return_value
593
+ mock_getpgrp.return_value = 1234
594
+
595
+ # Mock the global exit stack
596
+ mock_exit_stack = mock.Mock()
597
+ with mock.patch.object(cci, "_exit_stack", mock_exit_stack):
598
+ # Mock hasattr to return True for Unix functions
599
+ with mock.patch("builtins.hasattr", return_value=True):
600
+ # Call the signal handler with SIGINT
601
+ cci._signal_handler(signal.SIGINT, None)
602
+
603
+ # Verify console output
604
+ console_instance.print.assert_any_call(
605
+ "\n[yellow]Received SIGINT - CumulusCI is being terminated[/yellow]"
606
+ )
607
+
608
+ # Verify process group was terminated with SIGINT
609
+ mock_killpg.assert_called_once_with(1234, signal.SIGINT)
610
+
611
+ # Verify exit with correct code for SIGINT
612
+ mock_exit.assert_called_once_with(130)
613
+
614
+
615
+ @mock.patch("sys.exit")
616
+ @mock.patch("cumulusci.cli.cci.Console")
617
+ def test_signal_handler_prevents_recursion(mock_console, mock_exit):
618
+ """Test that the signal handler prevents recursive calls"""
619
+ console_instance = mock_console.return_value
620
+
621
+ # Set the flag to simulate handler already active
622
+ with mock.patch.object(cci, "_signal_handler_active", True):
623
+ # Call the signal handler
624
+ cci._signal_handler(signal.SIGTERM, None)
625
+
626
+ # Verify no console output (handler should return immediately)
627
+ console_instance.print.assert_not_called()
628
+
629
+ # Verify no exit call
630
+ mock_exit.assert_not_called()
631
+
632
+
633
+ @mock.patch("sys.exit")
634
+ @mock.patch("cumulusci.cli.cci.Console")
635
+ @mock.patch("os.killpg", create=True)
636
+ @mock.patch("os.getpgrp", create=True)
637
+ @mock.patch("signal.signal")
638
+ def test_signal_handler_handles_killpg_error(
639
+ mock_signal, mock_getpgrp, mock_killpg, mock_console, mock_exit
640
+ ):
641
+ """Test that the signal handler handles errors from killpg gracefully"""
642
+ console_instance = mock_console.return_value
643
+ mock_getpgrp.return_value = 1234
644
+ mock_killpg.side_effect = OSError("Process group not found")
645
+
646
+ # Mock hasattr to return True for Unix functions
647
+ with mock.patch("builtins.hasattr", return_value=True):
648
+ # Call the signal handler with SIGTERM
649
+ cci._signal_handler(signal.SIGTERM, None)
650
+
651
+ # Verify error message was printed
652
+ console_instance.print.assert_any_call(
653
+ "[red]Warning: Error terminating child processes: Process group not found[/red]"
654
+ )
655
+
656
+ # Verify it still exits with correct code
657
+ mock_exit.assert_called_once_with(143)
658
+
659
+
660
+ @mock.patch("sys.exit")
661
+ @mock.patch("cumulusci.cli.cci.Console")
662
+ def test_signal_handler_without_process_group_support(mock_console, mock_exit):
663
+ """Test that the signal handler works on Windows where process groups aren't supported"""
664
+ console_instance = mock_console.return_value
665
+
666
+ # Mock the global exit stack
667
+ mock_exit_stack = mock.Mock()
668
+ with mock.patch.object(cci, "_exit_stack", mock_exit_stack):
669
+ # Mock hasattr to return False for Unix functions (like on Windows)
670
+ with mock.patch("builtins.hasattr", return_value=False):
671
+ # Call the signal handler with SIGTERM
672
+ cci._signal_handler(signal.SIGTERM, None)
673
+
674
+ # Verify console output
675
+ console_instance.print.assert_any_call(
676
+ "\n[yellow]Received SIGTERM - CumulusCI is being terminated[/yellow]"
677
+ )
678
+ console_instance.print.assert_any_call(
679
+ "[yellow]Exiting with failure code due to external cancellation.[/yellow]"
680
+ )
681
+ console_instance.print.assert_any_call(
682
+ "[yellow]Terminating child processes...[/yellow]"
683
+ )
684
+ console_instance.print.assert_any_call(
685
+ "[yellow]Process group termination not supported on this platform[/yellow]"
686
+ )
687
+
688
+ # Verify cleanup was called
689
+ mock_exit_stack.close.assert_called_once()
690
+
691
+ # Verify exit with correct code
692
+ mock_exit.assert_called_once_with(143)
693
+
694
+
695
+ @mock.patch("os.setpgrp", create=True)
696
+ def test_main_creates_process_group(mock_setpgrp):
697
+ """Test that main() creates a new process group"""
698
+ # Mock dependencies to avoid actual CLI execution
699
+ with mock.patch.multiple(
700
+ "cumulusci.cli.cci",
701
+ signal=mock.Mock(),
702
+ init_requests_trust=mock.Mock(),
703
+ check_latest_version=mock.Mock(),
704
+ check_latest_plugins=mock.Mock(),
705
+ get_tempfile_logger=mock.Mock(return_value=(mock.Mock(), "tempfile.log")),
706
+ tee_stdout_stderr=mock.Mock(
707
+ return_value=mock.Mock(__enter__=mock.Mock(), __exit__=mock.Mock())
708
+ ),
709
+ set_debug_mode=mock.Mock(
710
+ return_value=mock.Mock(__enter__=mock.Mock(), __exit__=mock.Mock())
711
+ ),
712
+ CliRuntime=mock.Mock(),
713
+ init_logger=mock.Mock(),
714
+ cli=mock.Mock(),
715
+ ):
716
+ try:
717
+ cci.main(["cci", "version"])
718
+ except SystemExit:
719
+ pass # Expected for version command
720
+
721
+ # Verify process group was created
722
+ mock_setpgrp.assert_called_once()
723
+
724
+
725
+ @mock.patch("os.setpgrp", create=True)
726
+ def test_main_handles_setpgrp_error(mock_setpgrp):
727
+ """Test that main() handles setpgrp errors gracefully"""
728
+ mock_setpgrp.side_effect = OSError("Operation not permitted")
729
+
730
+ # Mock dependencies to avoid actual CLI execution
731
+ with mock.patch.multiple(
732
+ "cumulusci.cli.cci",
733
+ signal=mock.Mock(),
734
+ init_requests_trust=mock.Mock(),
735
+ check_latest_version=mock.Mock(),
736
+ check_latest_plugins=mock.Mock(),
737
+ get_tempfile_logger=mock.Mock(return_value=(mock.Mock(), "tempfile.log")),
738
+ tee_stdout_stderr=mock.Mock(
739
+ return_value=mock.Mock(__enter__=mock.Mock(), __exit__=mock.Mock())
740
+ ),
741
+ set_debug_mode=mock.Mock(
742
+ return_value=mock.Mock(__enter__=mock.Mock(), __exit__=mock.Mock())
743
+ ),
744
+ CliRuntime=mock.Mock(),
745
+ init_logger=mock.Mock(),
746
+ cli=mock.Mock(),
747
+ ):
748
+ try:
749
+ # Should not raise an exception even if setpgrp fails
750
+ cci.main(["cci", "version"])
751
+ except SystemExit:
752
+ pass # Expected for version command
753
+ except OSError:
754
+ pytest.fail("main() should handle setpgrp errors gracefully")
755
+
756
+ # Verify setpgrp was attempted
757
+ mock_setpgrp.assert_called_once()
758
+
759
+
760
+ @mock.patch("signal.signal")
761
+ def test_main_registers_signal_handlers(mock_signal):
762
+ """Test that main() registers signal handlers"""
763
+ # Create a proper context manager mock
764
+ context_manager_mock = mock.Mock()
765
+ context_manager_mock.__enter__ = mock.Mock(return_value=context_manager_mock)
766
+ context_manager_mock.__exit__ = mock.Mock(return_value=None)
767
+
768
+ # Create another context manager mock for set_debug_mode
769
+ debug_context_mock = mock.Mock()
770
+ debug_context_mock.__enter__ = mock.Mock(return_value=debug_context_mock)
771
+ debug_context_mock.__exit__ = mock.Mock(return_value=None)
772
+
773
+ # Mock dependencies to avoid actual CLI execution
774
+ with mock.patch.multiple(
775
+ "cumulusci.cli.cci",
776
+ init_requests_trust=mock.Mock(),
777
+ check_latest_version=mock.Mock(),
778
+ check_latest_plugins=mock.Mock(),
779
+ get_tempfile_logger=mock.Mock(return_value=(mock.Mock(), "tempfile.log")),
780
+ tee_stdout_stderr=mock.Mock(return_value=context_manager_mock),
781
+ set_debug_mode=mock.Mock(return_value=debug_context_mock),
782
+ CliRuntime=mock.Mock(),
783
+ init_logger=mock.Mock(),
784
+ cli=mock.Mock(),
785
+ ):
786
+ try:
787
+ cci.main(["cci", "version"])
788
+ except SystemExit:
789
+ pass # Expected for version command
790
+
791
+ # Verify signal handlers were registered
792
+ expected_calls = [
793
+ mock.call(signal.SIGTERM, cci._signal_handler),
794
+ mock.call(signal.SIGINT, cci._signal_handler),
795
+ ]
796
+ 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
@@ -373,20 +373,24 @@ class TestDownloadExtract:
373
373
 
374
374
  def test_set_target_directory__absolute_path(self):
375
375
  """Test _set_target_directory with absolute path"""
376
- absolute_path = "/tmp/test_dir"
377
- task_config = TaskConfig(
378
- {
379
- "options": {
380
- "repo_url": self.repo_url,
381
- "target_directory": absolute_path,
376
+ absolute_path = tempfile.mkdtemp(prefix="test_")
377
+ try:
378
+ task_config = TaskConfig(
379
+ {
380
+ "options": {
381
+ "repo_url": self.repo_url,
382
+ "target_directory": absolute_path,
383
+ }
382
384
  }
383
- }
384
- )
385
- task = DownloadExtract(self.project_config, task_config)
385
+ )
386
+ task = DownloadExtract(self.project_config, task_config)
386
387
 
387
- task._set_target_directory()
388
+ task._set_target_directory()
388
389
 
389
- assert task.options["target_directory"] == absolute_path
390
+ assert task.options["target_directory"] == absolute_path
391
+ finally:
392
+ # Clean up the temporary directory
393
+ os.rmdir(absolute_path)
390
394
 
391
395
  def test_rename_files(self):
392
396
  """Test _rename_files method"""
@@ -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.6
3
+ Version: 5.0.8.dev0
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=g6rB4S_8iDhKN5JGqjcBZgs-fUDE2THmw5wKLngtRVQ,22
1
+ cumulusci/__about__.py,sha256=nnWzpE6c_AzsihHEu6Ul-b8Oj0n88PpGCNkmHuwzmis,27
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=NcFm0Nng5o47fAMcjpSHjWRvQKDbD2lwPSkuoCVW9eA,11901
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=p9PYNij3dB3dNwOg9ivPSzVw179WORFVMfRpkWW68pg,27891
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
@@ -622,7 +622,7 @@ cumulusci/tasks/vcs/release.py,sha256=xXim12XVij34LRK6kAQ_5qeQLlYHNtx4e386doQCxh
622
622
  cumulusci/tasks/vcs/release_report.py,sha256=B8ehl6PK7kaEI_jS-rapQfNm0bhJRR7LBL4WFq1meNE,4226
623
623
  cumulusci/tasks/vcs/tag.py,sha256=x9Xw5t6g13ztl0TjtclQ1_JRBWtez73e9Az8oYJeqvU,1098
624
624
  cumulusci/tasks/vcs/tests/github/test_commit_status.py,sha256=1IAGz8_wFVUvk0wY5V_Aetb6M5Ri0nkOHaRSUdqQkZI,7206
625
- cumulusci/tasks/vcs/tests/github/test_download_extract.py,sha256=mpDYmuai0cF_p-PjPlY3MPMoDhk2r8QAcm4qZq2fOHQ,32420
625
+ cumulusci/tasks/vcs/tests/github/test_download_extract.py,sha256=_8j2xkypbF6xECkYgWby6vfHAss16FHhuy9flu-m5Fo,32594
626
626
  cumulusci/tasks/vcs/tests/github/test_merge.py,sha256=OS25YJHNVqfb2lPzpXOpzR8BEiDEh_C94BwDTvTDQ-U,39306
627
627
  cumulusci/tasks/vcs/tests/github/test_publish.py,sha256=kl3IvWmDL8M225R7Ygyr-vUjXQ0_GoUDH85kOncDlMk,30739
628
628
  cumulusci/tasks/vcs/tests/github/test_pull_request.py,sha256=dh-XihMjCH69wML6vg3KKqjB2NsfhBoMr0Ii1xfM8gw,974
@@ -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.6.dist-info/METADATA,sha256=oRRHu-EE-Kv7C4P-bjlInR9uzUoHMYbWNm8LwyfzhuM,6134
740
- cumulusci_plus-5.0.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
741
- cumulusci_plus-5.0.6.dist-info/entry_points.txt,sha256=nTtu04b9iLXhzADcTrb5PwmdXE6e2MTUAMh9OK6Z2pg,80
742
- cumulusci_plus-5.0.6.dist-info/licenses/AUTHORS.rst,sha256=PvewjKImdKPhhJ6xR2EEZ4T7GbpY2ZeAeyWm2aLtiMQ,676
743
- cumulusci_plus-5.0.6.dist-info/licenses/LICENSE,sha256=NFsF_s7RVXk2dU6tmRAN8wF45pnD98VZ5IwqOsyBcaU,1499
744
- cumulusci_plus-5.0.6.dist-info/RECORD,,
739
+ cumulusci_plus-5.0.8.dev0.dist-info/METADATA,sha256=No5M7E99WFdnfIqbyhyCUrW2WpLlo8W09i7yx6wgr7w,6139
740
+ cumulusci_plus-5.0.8.dev0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
741
+ cumulusci_plus-5.0.8.dev0.dist-info/entry_points.txt,sha256=nTtu04b9iLXhzADcTrb5PwmdXE6e2MTUAMh9OK6Z2pg,80
742
+ cumulusci_plus-5.0.8.dev0.dist-info/licenses/AUTHORS.rst,sha256=PvewjKImdKPhhJ6xR2EEZ4T7GbpY2ZeAeyWm2aLtiMQ,676
743
+ cumulusci_plus-5.0.8.dev0.dist-info/licenses/LICENSE,sha256=NFsF_s7RVXk2dU6tmRAN8wF45pnD98VZ5IwqOsyBcaU,1499
744
+ cumulusci_plus-5.0.8.dev0.dist-info/RECORD,,