Repository: PrefectHQ/prefect

Issue:
Merge task responds to a merge trigger
## Current behavior
*Please describe how the feature works today*
A merge task will run even if all upstream tasks are skipped. 


## Proposed behavior
*Please describe your proposed change to the current behavior*
If there are no tasks proceeding, a merge task should be skipped as well.



## Example
*Please give an example of how the enhancement would be useful*
If I have an ETL process with 3 conditional Transform tasks that aren't met, the downstream Load dependency should be skipped, since none of its upstream Transform tasks were completed. 


A merge task with only skipped ancestors should not be run. If I have an upstream conditional that is not met, the downstream dependency should be considered as also not met.


Trigger to be implemented (courtesy of @cicdw):
```
def merge_trigger(upstream_states):
    if all(state.is_skipped() for state in upstream_states):
        raise signals.SKIP("All upstreams skipped")
    elif not all(state.is_successful() for state in upstream_states):
        raise signals.TRIGGERFAIL(
            'Trigger was "all_successful" but some of the upstream tasks failed.'
        )
    return True
```


Discussion:

Resolving patch:
diff --git a/src/prefect/tasks/control_flow/conditional.py b/src/prefect/tasks/control_flow/conditional.py
--- a/src/prefect/tasks/control_flow/conditional.py
+++ b/src/prefect/tasks/control_flow/conditional.py
@@ -12,6 +12,7 @@ class Merge(Task):
     def __init__(self, **kwargs) -> None:
         if kwargs.setdefault("skip_on_upstream_skip", False):
             raise ValueError("Merge tasks must have `skip_on_upstream_skip=False`.")
+        kwargs.setdefault("trigger", prefect.triggers.not_all_skipped)
         super().__init__(**kwargs)
 
     def run(self, **task_results: Any) -> Any:
@@ -124,7 +125,8 @@ def merge(*tasks: Task) -> Task:
 
     A conditional branch in a flow results in one or more tasks proceeding and one or
     more tasks skipping. It is often convenient to merge those branches back into a
-    single result. This function is a simple way to achieve that goal.
+    single result. This function is a simple way to achieve that goal. By default this
+    task will skip if all its upstream dependencies are also skipped.
 
     The merge will return the first real result it encounters, or `None`. If multiple
     tasks might return a result, group them with a list.
diff --git a/src/prefect/triggers.py b/src/prefect/triggers.py
--- a/src/prefect/triggers.py
+++ b/src/prefect/triggers.py
@@ -247,5 +247,22 @@ def _some_successful(upstream_states: Set["state.State"]) -> bool:
     return _some_successful
 
 
+def not_all_skipped(upstream_states: Set["state.State"]) -> bool:
+    """
+    Runs if all upstream tasks were successful and were not all skipped.
+
+    Args:
+        - upstream_states (set[State]): the set of all upstream states
+    """
+
+    if all(state.is_skipped() for state in upstream_states):
+        raise signals.SKIP("All upstreams were skipped", result=None)
+    elif not all(state.is_successful() for state in upstream_states):
+        raise signals.TRIGGERFAIL(
+            'Trigger was "not_all_skipped" but some of the upstream tasks failed.'
+        )
+    return True
+
+
 # aliases
 always_run = all_finished  # type: Callable[[Set["state.State"]], bool]


Test patch:
diff --git a/tests/tasks/test_control_flow.py b/tests/tasks/test_control_flow.py
--- a/tests/tasks/test_control_flow.py
+++ b/tests/tasks/test_control_flow.py
@@ -118,6 +118,20 @@ def return_array():
     assert isinstance(flow_state.result[merge_task].result, SpecialObject)
 
 
+def test_merging_skips_if_all_upstreams_skip():
+    @task
+    def skip_task():
+        raise prefect.engine.signals.SKIP("not today")
+
+    with Flow("skipper") as flow:
+        merge_task = merge(skip_task(), skip_task())
+
+    flow_state = flow.run()
+    assert flow_state.is_successful()
+    assert flow_state.result[merge_task].is_skipped()
+    assert flow_state.result[merge_task].result is None
+
+
 def test_list_of_tasks():
     """
     Test that a list of tasks can be set as a switch condition
diff --git a/tests/test_triggers.py b/tests/test_triggers.py
--- a/tests/test_triggers.py
+++ b/tests/test_triggers.py
@@ -53,6 +53,33 @@ def test_all_successful_with_some_failed():
         triggers.all_successful(generate_states(failed=3, success=1))
 
 
+def test_not_all_skipped_with_all_success():
+    # True when all successful
+    assert triggers.not_all_skipped(generate_states(success=3))
+
+
+def test_not_all_skipped_with_all_skipped():
+    with pytest.raises(signals.SKIP) as exc:
+        assert triggers.not_all_skipped(generate_states(skipped=3))
+
+
+def test_not_all_skipped_with_all_success_or_skipped():
+    # True when all successful or skipped
+    assert triggers.not_all_skipped(generate_states(success=3, skipped=3))
+
+
+def test_not_all_skipped_with_all_failed():
+    # Fail when all fail
+    with pytest.raises(signals.TRIGGERFAIL):
+        triggers.not_all_skipped(generate_states(failed=3))
+
+
+def test_not_all_skipped_with_some_failed():
+    # Fail when some fail
+    with pytest.raises(signals.TRIGGERFAIL):
+        triggers.not_all_skipped(generate_states(failed=3, success=1))
+
+
 def test_all_failed_with_all_failed():
     assert triggers.all_failed(generate_states(failed=3))