Repository: PrefectHQ/prefect
Pull request: Raise `StepExecutionError` on non-zero `run_shell_script` return code during `prefect deploy`
This PR focuses on handling errors more effectively during the execution of `run_shell_script` in deployment steps. Instead of silently allowing deployment steps to continue during `prefect deploy`, we raise on error here. 

### Example
<!-- 
Share an example of the change in action.

A code blurb is best. Changes to features should include an example that is executable by a new user.
If changing documentation, a link to a preview of the page is great.
 -->

### Checklist
<!-- These boxes may be checked after opening the pull request. -->

- [x] This pull request references any related issue by including "closes `<link to issue>`"
	- If no issue exists and your change is not a small fix, please [create an issue](https://github.com/PrefectHQ/prefect/issues/new/choose) first.
- [x] This pull request includes tests or only affects documentation.
- [x] This pull request includes a label categorizing the change e.g. `maintenance`, `fix`, `feature`, `enhancement`, `docs`.
  <!-- If you do not have permission to add a label, a maintainer will add one for you -->

For documentation changes:

- [ ] This pull request includes redirect settings in `netlify.toml` for files that are removed or renamed


Problem statement:
Deployment steps continue when script in `run_shell_script` fails
### First check

- [X] I added a descriptive title to this issue.
- [X] I used the GitHub search to find a similar issue and didn't find it.
- [X] I searched the Prefect documentation for this issue.
- [X] I checked that this issue is related to Prefect and not one of its dependencies.

### Bug summary

Errors that occur during the execution of valid commands in `run_shell_script` do not interrupt the deployment process. Attempting to run invalid commands like `script: asdf` does interrupt the deployment process with a `No such file or directory` error.

My expectation is that deployment would fail similarly in both cases, or there should be a param for `run_shell_script` that allows me to choose whether I want an error in my script to cause deployment to fail.

### Reproduction

```python3
prefect.yaml

build:
  - prefect.deployments.steps.run_shell_script:
      script: cat nothing

push: null


pull: null


deployments:
  - name: my-deployment
    entrypoint: flow.py:my_flow
    work_pool:
      name: my-pool
```


### Error

```python3
prefect deploy -n my-deployment

Running deployment build steps...
 > Running run_shell_script step...
cat: nothing: No such file or directory

│ Deployment 'my-flow/my-deployment' successfully created with id '-'.
```


### Versions

```Text
Version:             2.14.10
API version:         0.8.4
Python version:      3.11.5
Git commit:          70888afe
Built:               Thu, Dec 7, 2023 7:27 PM
OS/Arch:             darwin/arm64
Profile:             default
Server type:         cloud
```


### Additional context

_No response_

Hints from issue discussion:
Thanks for creating an issue as we discussed in the [Slack thread](https://prefecthq.slack.com/archives/CQTTM2M0D/p1704382046332649)!

Commit message: update older test, add regression test

Patch to review:
diff --git a/src/prefect/deployments/steps/utility.py b/src/prefect/deployments/steps/utility.py
index da925be86b77..6bc52c481146 100644
--- a/src/prefect/deployments/steps/utility.py
+++ b/src/prefect/deployments/steps/utility.py
@@ -188,6 +188,12 @@ async def run_shell_script(
 
             await process.wait()
 
+            if process.returncode != 0:
+                raise RuntimeError(
+                    f"run_shell_script failed with error code {process.returncode}:"
+                    f" {stderr_sink.getvalue()}"
+                )
+
     return {
         "stdout": stdout_sink.getvalue().strip(),
         "stderr": stderr_sink.getvalue().strip(),
diff --git a/tests/cli/test_deploy.py b/tests/cli/test_deploy.py
index bb0ab990204f..ba4e235edfca 100644
--- a/tests/cli/test_deploy.py
+++ b/tests/cli/test_deploy.py
@@ -29,6 +29,7 @@
     create_default_prefect_yaml,
     initialize_project,
 )
+from prefect.deployments.steps.core import StepExecutionError
 from prefect.events.schemas import Posture
 from prefect.exceptions import ObjectAlreadyExists, ObjectNotFound
 from prefect.infrastructure.container import DockerRegistry
@@ -2397,6 +2398,39 @@ async def test_deploy_does_not_prompt_build_docker_image_when_empty_build_action
             "An important name/test-name"
         )
 
+    async def test_deploy_with_bad_run_shell_script_raises(
+        self, project_dir, work_pool
+    ):
+        """
+        Regression test for a bug where deployment steps woould continue even when
+        a `run_shell_script` step failed.
+        """
+        prefect_file = Path("prefect.yaml")
+        with prefect_file.open(mode="r") as f:
+            config = yaml.safe_load(f)
+
+        config["build"] = [
+            {
+                "prefect.deployments.steps.run_shell_script": {
+                    "id": "test",
+                    "script": "cat nothing",
+                    "stream_output": True,
+                }
+            }
+        ]
+
+        with prefect_file.open(mode="w") as f:
+            yaml.safe_dump(config, f)
+
+        with pytest.raises(StepExecutionError):
+            await run_sync_in_worker_thread(
+                invoke_and_assert,
+                command=(
+                    "deploy ./flows/hello.py:my_flow -n test-name --pool"
+                    f" {work_pool.name}"
+                ),
+            )
+
     @pytest.mark.usefixtures("project_dir")
     async def test_deploy_templates_env_vars(
         self, prefect_client, monkeypatch, work_pool
@@ -6006,8 +6040,8 @@ async def test_other_build_step_exists_prompts_build_custom_docker_image(
         prefect_config["build"] = [
             {
                 "prefect.deployments.steps.run_shell_script": {
-                    "id": "get-commit-hash",
-                    "script": "git rev-parse --short HEAD",
+                    "id": "sample-bash-cmd",
+                    "script": "echo 'Hello, World!'",
                     "stream_output": False,
                 }
             }


Code review comments:
diff_hunk: @@ -188,6 +188,12 @@ async def run_shell_script(
 
             await process.wait()
 
+            if process.returncode != 0:
+                raise RuntimeError(
+                    f"run_shell_script failed with error code {process.returncode}:"
path: src/prefect/deployments/steps/utility.py
text: @user1:
nit
```suggestion
                    f"`run_shell_script` failed with error code {process.returncode}:"
```