diff --git a/cumulusci/cli/flow.py b/cumulusci/cli/flow.py index 96bd8db9cf..27fcc05702 100644 --- a/cumulusci/cli/flow.py +++ b/cumulusci/cli/flow.py @@ -44,9 +44,9 @@ def flow_doc(runtime, project=False): flows_by_group = group_items(flows) flow_groups = sorted( flows_by_group.keys(), - key=lambda group: flow_info_groups.index(group) - if group in flow_info_groups - else 100, + key=lambda group: ( + flow_info_groups.index(group) if group in flow_info_groups else 100 + ), ) for group in flow_groups: @@ -106,10 +106,39 @@ def flow_list(runtime, plain, print_json): @flow.command(name="info", help="Displays information for a flow") @click.argument("flow_name") +@click.option( + "--skip", help="Specify a comma separated list of task and flow names to skip." +) +@click.option( + "--skip-from", + help="Specify a task or flow name to skip and all steps that follow it.", +) +@click.option( + "--start-from", + help="Specify a task or flow name to start from. All prior steps will be skippped.", +) +@click.option( + "--load-yml", + help="If set, loads the specified yml file into the the project config as additional config", +) @pass_runtime(require_keychain=True) -def flow_info(runtime, flow_name): +def flow_info( + runtime, + flow_name, + skip=None, + skip_from=None, + start_from=None, + load_yml=None, +): + if skip: + skip = skip.split(",") try: - coordinator = runtime.get_flow(flow_name) + coordinator = runtime.get_flow( + flow_name, + skip=skip, + skip_from=skip_from, + start_from=start_from, + ) output = coordinator.get_summary(verbose=True) click.echo(output) except FlowNotFoundError as e: @@ -141,9 +170,37 @@ def flow_info(runtime, flow_name): is_flag=True, help="Disables all prompts. Set for non-interactive mode use such as calling from scripts or CI systems", ) +@click.option( + "--skip", help="Specify a comma separated list of task and flow names to skip." +) +@click.option( + "--skip-from", + help="Specify a task or flow name to skip and all steps that follow it.", +) +@click.option( + "--start-from", + help="Specify a task or flow name to start from. All prior steps will be skippped.", +) +@click.option( + "--load-yml", + help="If set, loads the specified yml file into the the project config as additional config", +) @pass_runtime(require_keychain=True) -def flow_run(runtime, flow_name, org, delete_org, debug, o, no_prompt): - +def flow_run( + runtime, + flow_name, + org, + delete_org, + debug, + o, + no_prompt, + skip=None, + skip_from=None, + start_from=None, + load_yml=None, +): + if skip: + skip = skip.split(",") # Get necessary configs org, org_config = runtime.get_org(org) if delete_org and not org_config.scratch: @@ -163,7 +220,13 @@ def flow_run(runtime, flow_name, org, delete_org, debug, o, no_prompt): # Create the flow and handle initialization exceptions try: - coordinator = runtime.get_flow(flow_name, options=options) + coordinator = runtime.get_flow( + flow_name, + options=options, + skip=skip, + skip_from=skip_from, + start_from=start_from, + ) start_time = datetime.now() coordinator.run(org_config) duration = datetime.now() - start_time diff --git a/cumulusci/core/flowrunner.py b/cumulusci/core/flowrunner.py index 098e84ba4d..f6a5aec98b 100644 --- a/cumulusci/core/flowrunner.py +++ b/cumulusci/core/flowrunner.py @@ -109,6 +109,9 @@ class StepSpec: "allow_failure", "path", "skip", + "skip_steps", + "skip_from", + "start_from", "when", ) @@ -122,6 +125,9 @@ class StepSpec: allow_failure: bool path: str skip: bool + skip_steps: List[str] + skip_from: str + start_from: str when: Optional[str] def __init__( @@ -134,6 +140,9 @@ def __init__( allow_failure: bool = False, from_flow: Optional[str] = None, skip: bool = False, + skip_from: Optional[str] = None, + skip_steps: Optional[List[str]] = None, + start_from: Optional[str] = None, when: Optional[str] = None, ): self.step_num = step_num @@ -143,6 +152,9 @@ def __init__( self.project_config = project_config self.allow_failure = allow_failure self.skip = skip + self.skip_from = skip_from + self.skip_steps = skip_steps or [] + self.start_from = start_from self.when = when # Store the dotted path to this step. @@ -326,6 +338,7 @@ class FlowCoordinator: callbacks: FlowCallback logger: logging.Logger skip: List[str] + skip_from: List[str] flow_config: FlowConfig runtime_options: dict name: Optional[str] @@ -338,6 +351,8 @@ def __init__( name: Optional[str] = None, options: Optional[dict] = None, skip: Optional[List[str]] = None, + skip_from: Optional[List[str]] = None, + start_from: Optional[str] = None, callbacks: Optional[FlowCallback] = None, ): self.project_config = project_config @@ -352,6 +367,10 @@ def __init__( self.runtime_options = options or {} self.skip = skip or [] + self.skip_from = skip_from or None + self.start_from = start_from + self.skip_beginning = start_from is not None + self.skip_remaining = False self.results = [] self.logger = self._init_logger() @@ -426,7 +445,11 @@ def get_flow_steps( if for_docs: source = "" - lines.append(f"{' ' * i}{steps[i]}) flow: {flow_name}{source}") + line = f"{' ' * i}{steps[i]}) flow: {flow_name}{source}" + if step.skip: + line += " [skip]" + line = f"\033[90m{line}\033[0m" # Gray color + lines.append(line) if source: new_source = "" @@ -444,9 +467,11 @@ def get_flow_steps( for option, value in options.items(): options_info += f"\n{padding} {option}: {value}" - lines.append( - f"{' ' * (i + 1)}{steps[i + 1]}) task: {task_name}{new_source}" - ) + line = f"{' ' * (i + 1)}{steps[i + 1]}) task: {task_name}{new_source}" + if step.skip: + line += " [skip]" + line = f"\033[90m{line}\033[0m" # Gray color + lines.append(line) if when: lines.append(when) @@ -600,13 +625,20 @@ def _visit_step( assert step_config.keys() != {"task", "flow"} # Skips - # - either in YAML (with the None string) + # - either in YAML (with the None string for task or flow on a step) or in the skip list on a flow step # - or by providing a skip list to the FlowRunner at initialization. + task_or_flow = step_config.get("task", step_config.get("flow")) + if task_or_flow and task_or_flow == self.start_from: + self.skip_beginning = False if ( - ("flow" in step_config and step_config["flow"] == "None") - or ("task" in step_config and step_config["task"] == "None") - or ("task" in step_config and step_config["task"] in self.skip) + task_or_flow == "None" + or task_or_flow in self.skip + or task_or_flow == self.skip_from + or self.skip_remaining + or self.skip_beginning ): + if task_or_flow == self.skip_from: + self.skip_remaining = True visited_steps.append( StepSpec( step_num=step_number, diff --git a/cumulusci/core/runtime.py b/cumulusci/core/runtime.py index d5b2ce3459..444b0f5be2 100644 --- a/cumulusci/core/runtime.py +++ b/cumulusci/core/runtime.py @@ -1,6 +1,6 @@ import sys from abc import abstractmethod -from typing import Optional, Type +from typing import List, Optional, Type from cumulusci.core.config import BaseProjectConfig, UniversalConfig from cumulusci.core.debug import DebugMode, get_debug_mode @@ -98,7 +98,14 @@ def _load_keychain(self): self.keychain = self.keychain_cls(self.project_config, keychain_key) self.project_config.keychain = self.keychain - def get_flow(self, name: str, options: Optional[dict] = None) -> FlowCoordinator: + def get_flow( + self, + name: str, + options: Optional[dict] = None, + skip: Optional[List[str]] = None, + skip_from: Optional[str] = None, + start_from: Optional[str] = None, + ) -> FlowCoordinator: """Get a primed and ready-to-go flow coordinator.""" if not self.project_config: raise ProjectConfigNotFound @@ -109,7 +116,9 @@ def get_flow(self, name: str, options: Optional[dict] = None) -> FlowCoordinator flow_config, name=flow_config.name, options=options, - skip=None, + skip=skip or flow_config.skip, + skip_from=skip_from or flow_config.skip_from, + start_from=start_from or flow_config.start_from, callbacks=callbacks, ) return coordinator diff --git a/cumulusci/schema/cumulusci.jsonschema.json b/cumulusci/schema/cumulusci.jsonschema.json index 255d2de4d4..f3041bdf97 100644 --- a/cumulusci/schema/cumulusci.jsonschema.json +++ b/cumulusci/schema/cumulusci.jsonschema.json @@ -200,6 +200,21 @@ "group": { "title": "Group", "type": "string" + }, + "skip_steps": { + "title": "Skip Steps", + "type": "array", + "items": { + "type": "string" + } + }, + "skip_from": { + "title": "Skip From", + "type": "string" + }, + "start_from": { + "title": "Start From", + "type": "string" } }, "additionalProperties": false diff --git a/cumulusci/utils/yaml/cumulusci_yml.py b/cumulusci/utils/yaml/cumulusci_yml.py index f8498ed9ea..86b90f6ffd 100644 --- a/cumulusci/utils/yaml/cumulusci_yml.py +++ b/cumulusci/utils/yaml/cumulusci_yml.py @@ -74,6 +74,9 @@ class Flow(CCIDictModel): description: str = None steps: Dict[str, Step] = None group: str = None + skip_steps: Optional[List[str]] = None + skip_from: Optional[str] = None + start_from: Optional[str] = None class Package(CCIDictModel):