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 +1 -1
- cumulusci/cli/cci.py +95 -5
- cumulusci/cli/tests/test_cci.py +254 -14
- cumulusci/core/dependencies/base.py +124 -4
- cumulusci/core/dependencies/tests/test_dependencies.py +202 -1
- cumulusci/core/tests/test_utils.py +151 -0
- cumulusci/core/utils.py +23 -0
- cumulusci/schema/cumulusci.jsonschema.json +8 -0
- cumulusci/utils/yaml/cumulusci_yml.py +2 -0
- cumulusci/vcs/vcs_source.py +1 -0
- {cumulusci_plus-5.0.5.dist-info → cumulusci_plus-5.0.8.dist-info}/METADATA +1 -1
- {cumulusci_plus-5.0.5.dist-info → cumulusci_plus-5.0.8.dist-info}/RECORD +16 -16
- {cumulusci_plus-5.0.5.dist-info → cumulusci_plus-5.0.8.dist-info}/WHEEL +0 -0
- {cumulusci_plus-5.0.5.dist-info → cumulusci_plus-5.0.8.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.5.dist-info → cumulusci_plus-5.0.8.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.5.dist-info → cumulusci_plus-5.0.8.dist-info}/licenses/LICENSE +0 -0
cumulusci/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "5.0.
|
|
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
|
-
|
|
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(
|
cumulusci/cli/tests/test_cci.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import io
|
|
3
3
|
import os
|
|
4
|
-
import
|
|
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.
|
|
33
|
-
"environ_mock": mock.patch.dict(
|
|
34
|
-
os.environ, {"HOME": tempfile.mkdtemp(), "CUMULUSCI_KEY": ""}
|
|
35
|
-
),
|
|
32
|
+
"tempdir": tempfile.gettempdir(),
|
|
36
33
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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(
|
|
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
|
|
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
|
|
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):
|
cumulusci/vcs/vcs_source.py
CHANGED
|
@@ -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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
740
|
-
cumulusci_plus-5.0.
|
|
741
|
-
cumulusci_plus-5.0.
|
|
742
|
-
cumulusci_plus-5.0.
|
|
743
|
-
cumulusci_plus-5.0.
|
|
744
|
-
cumulusci_plus-5.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|