Repository: PrefectHQ/prefect

Issue:
Prompt for flow code storage options in `prefect deploy` interactive CLI
### Summary

When a user runs `prefect deploy` without an existing `pull` deployment action defined in `prefect.yaml`, and `prefect deploy` is either being run in a directory that is not a git repository, or a user has declined having their workers pull from a remote repository, the user should be shown a list of services that can be used to store their code for their workers to later retrieve from.

The service should correspond to `push` and `pull` steps that are available from Prefect integration packages. To start, the user should be shown the following services:
- AWS S3
- Google Cloud Storage
- Azure Blob Storage

When a service is selected, the user should be prompted for service-specific details necessary for the storage and retrieval of code (e.g. bucket, folder, credentials). If credentials are collected, the corresponding credentials block (e.g. `AwsCredentials` for S3) should be created. The information gathered from the use should be used to create `push` and `pull` steps for the configured service.

### Implementation Notes

In order to generate prompts for the different services, we'll likely need to have some metadata related to each step to determine which information to gather. Long term, we'll probably want to store that metadata separate from the `prefect` repo, but we can store the metadata in the `prefect` repo for the first iteration, if easier.


Discussion:

Resolving patch:
diff --git a/src/prefect/cli/_prompts.py b/src/prefect/cli/_prompts.py
--- a/src/prefect/cli/_prompts.py
+++ b/src/prefect/cli/_prompts.py
@@ -18,7 +18,7 @@
 from prefect.cli._utilities import exit_with_error
 from prefect.client.collections import get_collections_metadata_client
 from prefect.client.orchestration import PrefectClient
-from prefect.client.schemas.actions import WorkPoolCreate
+from prefect.client.schemas.actions import BlockDocumentCreate, WorkPoolCreate
 from prefect.client.schemas.schedules import (
     SCHEDULE_TYPES,
     CronSchedule,
@@ -26,12 +26,29 @@
     RRuleSchedule,
 )
 from prefect.client.utilities import inject_client
-from prefect.deployments.base import _search_for_flow_functions
+from prefect.deployments.base import (
+    _get_git_remote_origin_url,
+    _search_for_flow_functions,
+)
+from prefect.exceptions import ObjectAlreadyExists, ObjectNotFound
 from prefect.flows import load_flow_from_entrypoint
 from prefect.infrastructure.container import DockerRegistry
+from prefect.settings import PREFECT_UI_URL
 from prefect.utilities.processutils import get_sys_executable, run_process
 from prefect.utilities.slugify import slugify
 
+STORAGE_PROVIDER_TO_CREDS_BLOCK = {
+    "s3": "aws-credentials",
+    "gcs": "gcp-credentials",
+    "azure_blob_storage": "azure-blob-storage-credentials",
+}
+
+REQUIRED_FIELDS_FOR_CREDS_BLOCK = {
+    "aws-credentials": ["aws_access_key_id", "aws_secret_access_key"],
+    "gcp-credentials": ["project", "service_account_file"],
+    "azure-blob-storage-credentials": ["account_url", "connection_string"],
+}
+
 
 def prompt(message, **kwargs):
     """Utility to prompt the user for input with consistent styling"""
@@ -623,3 +640,158 @@ async def prompt_entrypoint(console: Console) -> str:
             console=console,
         )
     return f"{selected_flow['filepath']}:{selected_flow['function_name']}"
+
+
+@inject_client
+async def prompt_select_remote_flow_storage(
+    console: Console, client: PrefectClient = None
+) -> Optional[str]:
+    valid_slugs_for_context = set()
+
+    for (
+        storage_provider,
+        creds_block_type_slug,
+    ) in STORAGE_PROVIDER_TO_CREDS_BLOCK.items():
+        try:
+            # only return storage options for which the user has a credentials
+            # block type
+            await client.read_block_type_by_slug(creds_block_type_slug)
+            valid_slugs_for_context.add(storage_provider)
+        except ObjectNotFound:
+            pass
+
+    if _get_git_remote_origin_url():
+        valid_slugs_for_context.add("git")
+
+    flow_storage_options = [
+        {
+            "type": "Git Repo",
+            "slug": "git",
+            "description": "Use a Git repository [bold](recommended).",
+        },
+        {
+            "type": "S3",
+            "slug": "s3",
+            "description": "Use an AWS S3 bucket.",
+        },
+        {
+            "type": "GCS",
+            "slug": "gcs",
+            "description": "Use a Google Cloud Storage bucket.",
+        },
+        {
+            "type": "Azure Blob Storage",
+            "slug": "azure_blob_storage",
+            "description": "Use an Azure Blob Storage bucket.",
+        },
+    ]
+
+    valid_storage_options_for_context = [
+        row
+        for row in flow_storage_options
+        if row is not None and row["slug"] in valid_slugs_for_context
+    ]
+
+    selected_flow_storage_row = prompt_select_from_table(
+        console,
+        prompt="Please select a remote code storage option.",
+        columns=[
+            {"header": "Storage Type", "key": "type"},
+            {"header": "Description", "key": "description"},
+        ],
+        data=valid_storage_options_for_context,
+    )
+
+    return selected_flow_storage_row["slug"]
+
+
+@inject_client
+async def prompt_select_blob_storage_credentials(
+    console: Console, storage_provider: str, client: PrefectClient = None
+) -> str:
+    """
+    Prompt the user for blob storage credentials.
+
+    Returns a jinja template string that references a credentials block.
+    """
+
+    storage_provider_slug = storage_provider.replace("_", "-")
+    pretty_storage_provider = storage_provider.replace("_", " ").upper()
+
+    creds_block_type_slug = STORAGE_PROVIDER_TO_CREDS_BLOCK[storage_provider]
+    pretty_creds_block_type = creds_block_type_slug.replace("-", " ").title()
+
+    existing_credentials_blocks = await client.read_block_documents_by_type(
+        block_type_slug=creds_block_type_slug
+    )
+
+    if existing_credentials_blocks:
+        selected_credentials_block = prompt_select_from_table(
+            console,
+            prompt=(
+                f"Select from your existing {pretty_creds_block_type} credential blocks"
+            ),
+            columns=[
+                {
+                    "header": f"{pretty_storage_provider} Credentials Blocks",
+                    "key": "name",
+                }
+            ],
+            data=[{"name": block.name} for block in existing_credentials_blocks],
+            opt_out_message="Create a new credentials block",
+        )
+
+        if selected_credentials_block and (
+            selected_block := selected_credentials_block.get("name")
+        ):
+            return f"{{{{ prefect.blocks.{creds_block_type_slug}.{selected_block} }}}}"
+
+    credentials_block_type = await client.read_block_type_by_slug(creds_block_type_slug)
+
+    credentials_block_schema = await client.get_most_recent_block_schema_for_block_type(
+        block_type_id=credentials_block_type.id
+    )
+
+    console.print(
+        f"\nProvide details on your new {pretty_storage_provider} credentials:"
+    )
+
+    hydrated_fields = {
+        field_name: prompt(f"{field_name} [yellow]({props.get('type')})[/]")
+        for field_name, props in credentials_block_schema.fields.get(
+            "properties"
+        ).items()
+        if field_name in REQUIRED_FIELDS_FOR_CREDS_BLOCK[creds_block_type_slug]
+    }
+
+    console.print(f"[blue]\n{pretty_storage_provider} credentials specified![/]\n")
+
+    while True:
+        credentials_block_name = prompt(
+            "Give a name to your new credentials block",
+            default=f"{storage_provider_slug}-storage-credentials",
+        )
+
+        try:
+            new_block_document = await client.create_block_document(
+                block_document=BlockDocumentCreate(
+                    name=credentials_block_name,
+                    data=hydrated_fields,
+                    block_schema_id=credentials_block_schema.id,
+                    block_type_id=credentials_block_type.id,
+                )
+            )
+            break
+        except ObjectAlreadyExists:
+            console.print(
+                f"A {pretty_creds_block_type!r} block named"
+                f" {credentials_block_name!r} already exists. Please choose another"
+                " name"
+            )
+
+    if PREFECT_UI_URL:
+        console.print(
+            "\nView/Edit your new credentials block in the UI:"
+            f"\n[blue]{PREFECT_UI_URL.value()}/blocks/block/{new_block_document.id}[/]\n"
+        )
+    return f"{{{{ prefect.blocks.{creds_block_type_slug}.{new_block_document.name} }}}}"
diff --git a/src/prefect/cli/deploy.py b/src/prefect/cli/deploy.py
--- a/src/prefect/cli/deploy.py
+++ b/src/prefect/cli/deploy.py
@@ -26,7 +26,9 @@
     prompt_entrypoint,
     prompt_push_custom_docker_image,
     prompt_schedule,
+    prompt_select_blob_storage_credentials,
     prompt_select_from_table,
+    prompt_select_remote_flow_storage,
     prompt_select_work_pool,
 )
 from prefect.cli._utilities import (
@@ -479,6 +481,11 @@ async def _run_single_deploy(
         "prefect_docker.projects.steps.build_docker_image",
     ]
 
+    docker_push_steps = [
+        "prefect_docker.deployments.steps.push_docker_image",
+        "prefect_docker.projects.steps.push_docker_image",
+    ]
+
     docker_build_step_exists = any(
         any(step in action for step in docker_build_steps)
         for action in deploy_config.get("build", actions.get("build")) or []
@@ -526,6 +533,31 @@ async def _run_single_deploy(
             build_steps = deploy_config.get("build", actions.get("build")) or []
             push_steps = deploy_config.get("push", actions.get("push")) or []
 
+    docker_push_step_exists = any(
+        any(step in action for step in docker_push_steps)
+        for action in deploy_config.get("push", actions.get("push")) or []
+    )
+
+    ## CONFIGURE PUSH and/or PULL STEPS FOR REMOTE FLOW STORAGE
+    if (
+        is_interactive()
+        and not ci
+        and not deploy_config.get("pull")
+        and not docker_push_step_exists
+        and confirm(
+            (
+                "Your Prefect workers will need access to this flow's code in order to"
+                " run it. Would you like your workers to pull your flow code from a"
+                " remote storage location when running this flow?"
+            ),
+            default=True,
+            console=app.console,
+        )
+    ):
+        actions = await _generate_actions_for_remote_flow_storage(
+            console=app.console, deploy_config=deploy_config, actions=actions
+        )
+
     if trigger_specs := _gather_deployment_trigger_definitions(
         options.get("triggers"), deploy_config.get("triggers")
     ):
@@ -544,6 +576,17 @@ async def _run_single_deploy(
     else:
         triggers = []
 
+    pull_steps = (
+        deploy_config.get("pull")
+        or actions.get("pull")
+        or await _generate_default_pull_action(
+            app.console,
+            deploy_config=deploy_config,
+            actions=actions,
+            ci=ci,
+        )
+    )
+
     ## RUN BUILD AND PUSH STEPS
     step_outputs = {}
     if build_steps:
@@ -552,7 +595,7 @@ async def _run_single_deploy(
             await run_steps(build_steps, step_outputs, print_function=app.console.print)
         )
 
-    if push_steps:
+    if push_steps := push_steps or actions.get("push"):
         app.console.print("Running deployment push steps...")
         step_outputs.update(
             await run_steps(push_steps, step_outputs, print_function=app.console.print)
@@ -580,15 +623,6 @@ async def _run_single_deploy(
     deploy_config["parameter_openapi_schema"] = _parameter_schema
     deploy_config["schedule"] = _schedule
 
-    # prepare the pull step
-    pull_steps = deploy_config.get(
-        "pull", actions.get("pull")
-    ) or await _generate_default_pull_action(
-        app.console,
-        deploy_config=deploy_config,
-        actions=actions,
-        ci=ci,
-    )
     pull_steps = apply_values(pull_steps, step_outputs, remove_notset=False)
 
     flow_id = await client.create_flow_from_name(deploy_config["flow_name"])
@@ -847,7 +881,12 @@ async def _generate_git_clone_pull_step(
 ):
     branch = _get_git_branch() or "main"
 
-    if not confirm(
+    if not remote_url:
+        remote_url = prompt(
+            "Please enter the URL to pull your flow code from", console=console
+        )
+
+    elif not confirm(
         f"Is [green]{remote_url}[/] the correct URL to pull your flow code from?",
         default=True,
         console=console,
@@ -961,10 +1000,65 @@ async def _check_for_build_docker_image_step(
     return None
 
 
+async def _generate_actions_for_remote_flow_storage(
+    console: Console, deploy_config: dict, actions: List[Dict]
+) -> Dict[str, List[Dict[str, Any]]]:
+    storage_provider_to_collection = {
+        "s3": "prefect_aws",
+        "gcs": "prefect_gcp",
+        "azure_blob_storage": "prefect_azure",
+    }
+    selected_storage_provider = await prompt_select_remote_flow_storage(console=console)
+
+    if selected_storage_provider == "git":
+        actions["pull"] = await _generate_git_clone_pull_step(
+            console=console,
+            deploy_config=deploy_config,
+            remote_url=_get_git_remote_origin_url(),
+        )
+
+    elif selected_storage_provider in storage_provider_to_collection.keys():
+        collection = storage_provider_to_collection[selected_storage_provider]
+
+        bucket, folder = prompt("Bucket name"), prompt("Folder name")
+
+        credentials = await prompt_select_blob_storage_credentials(
+            console=console,
+            storage_provider=selected_storage_provider,
+        )
+
+        step_fields = {
+            (
+                "container"
+                if selected_storage_provider == "azure_blob_storage"
+                else "bucket"
+            ): bucket,
+            "folder": folder,
+            "credentials": credentials,
+        }
+
+        actions["push"] = [
+            {
+                f"{collection}.deployments.steps.push_to_{selected_storage_provider}": (
+                    step_fields
+                )
+            }
+        ]
+
+        actions["pull"] = [
+            {
+                f"{collection}.deployments.steps.pull_from_{selected_storage_provider}": (
+                    step_fields
+                )
+            }
+        ]
+
+    return actions
+
+
 async def _generate_default_pull_action(
     console: Console, deploy_config: Dict, actions: List[Dict], ci: bool = False
 ):
-    remote_url = _get_git_remote_origin_url()
     build_docker_image_step = await _check_for_build_docker_image_step(
         deploy_config.get("build") or actions["build"]
     )
@@ -974,44 +1068,19 @@ async def _generate_default_pull_action(
             return await _generate_pull_step_for_build_docker_image(
                 console, deploy_config
             )
-        else:
-            if is_interactive():
-                if remote_url and confirm(
-                    "Would you like to pull your flow code from its remote"
-                    " repository when running your deployment?"
-                ):
-                    return await _generate_git_clone_pull_step(
-                        console, deploy_config, remote_url
-                    )
-                if not confirm(
-                    "Does your Dockerfile have a line that copies the current working"
-                    " directory into your image?"
-                ):
-                    exit_with_error(
-                        "Your flow code must be copied into your Docker image to run"
-                        " your deployment.\nTo do so, you can copy this line into your"
-                        " Dockerfile: [yellow]COPY . /opt/prefect/[/yellow]"
-                    )
-                return await _generate_pull_step_for_build_docker_image(
-                    console, deploy_config, auto=False
+        if is_interactive() and not ci:
+            if not confirm(
+                "Does your Dockerfile have a line that copies the current working"
+                " directory into your image?"
+            ):
+                exit_with_error(
+                    "Your flow code must be copied into your Docker image to run"
+                    " your deployment.\nTo do so, you can copy this line into your"
+                    " Dockerfile: [yellow]COPY . /opt/prefect/[/yellow]"
                 )
-
-    if (
-        is_interactive()
-        and not ci
-        and remote_url
-        and confirm(
-            (
-                "Your Prefect workers will need access to this flow's code in order to"
-                " run it. Would you like your workers to pull your flow code from its"
-                " remote repository when running this flow?"
-            ),
-            default=True,
-            console=console,
-        )
-    ):
-        return await _generate_git_clone_pull_step(console, deploy_config, remote_url)
-
+            return await _generate_pull_step_for_build_docker_image(
+                console, deploy_config, auto=False
+            )
     else:
         entrypoint_path, _ = deploy_config["entrypoint"].split(":")
         console.print(
diff --git a/src/prefect/client/orchestration.py b/src/prefect/client/orchestration.py
--- a/src/prefect/client/orchestration.py
+++ b/src/prefect/client/orchestration.py
@@ -1308,6 +1308,34 @@ async def read_block_schemas(self) -> List[BlockSchema]:
         response = await self._client.post("/block_schemas/filter", json={})
         return pydantic.parse_obj_as(List[BlockSchema], response.json())
 
+    async def get_most_recent_block_schema_for_block_type(
+        self,
+        block_type_id: UUID,
+    ) -> Optional[BlockSchema]:
+        """
+        Fetches the most recent block schema for a specified block type ID.
+
+        Args:
+            block_type_id: The ID of the block type.
+
+        Raises:
+            httpx.RequestError: If the request fails for any reason.
+
+        Returns:
+            The most recent block schema or None.
+        """
+        try:
+            response = await self._client.post(
+                "/block_schemas/filter",
+                json={
+                    "block_schemas": {"block_type_id": {"any_": [str(block_type_id)]}},
+                    "limit": 1,
+                },
+            )
+        except httpx.HTTPStatusError:
+            raise
+        return BlockSchema.parse_obj(response.json()[0]) if response.json() else None
+
     async def read_block_document(
         self,
         block_document_id: UUID,
@@ -1419,6 +1447,35 @@ async def read_block_documents(
         )
         return pydantic.parse_obj_as(List[BlockDocument], response.json())
 
+    async def read_block_documents_by_type(
+        self,
+        block_type_slug: str,
+        offset: Optional[int] = None,
+        limit: Optional[int] = None,
+        include_secrets: bool = True,
+    ) -> List[BlockDocument]:
+        """Retrieve block documents by block type slug.
+
+        Args:
+            block_type_slug: The block type slug.
+            offset: an offset
+            limit: the number of blocks to return
+            include_secrets: whether to include secret values
+
+        Returns:
+            A list of block documents
+        """
+        response = await self._client.get(
+            f"/block_types/slug/{block_type_slug}/block_documents",
+            params=dict(
+                offset=offset,
+                limit=limit,
+                include_secrets=include_secrets,
+            ),
+        )
+
+        return pydantic.parse_obj_as(List[BlockDocument], response.json())
+
     async def create_deployment(
         self,
         flow_id: UUID,


Test patch:
diff --git a/tests/cli/test_deploy.py b/tests/cli/test_deploy.py
--- a/tests/cli/test_deploy.py
+++ b/tests/cli/test_deploy.py
@@ -30,10 +30,16 @@
     initialize_project,
 )
 from prefect.events.schemas import Posture
-from prefect.exceptions import ObjectNotFound
+from prefect.exceptions import ObjectAlreadyExists, ObjectNotFound
 from prefect.infrastructure.container import DockerRegistry
-from prefect.server.schemas.actions import WorkPoolCreate
+from prefect.server.schemas.actions import (
+    BlockDocumentCreate,
+    BlockSchemaCreate,
+    BlockTypeCreate,
+    WorkPoolCreate,
+)
 from prefect.server.schemas.schedules import CronSchedule
+from prefect.settings import PREFECT_UI_URL, temporary_settings
 from prefect.testing.cli import invoke_and_assert
 from prefect.testing.utilities import AsyncMock
 from prefect.utilities.asyncutils import run_sync_in_worker_thread
@@ -144,9 +150,12 @@ def uninitialized_project_dir_with_git_with_remote(
 
 @pytest.fixture
 async def default_agent_pool(prefect_client):
-    return await prefect_client.create_work_pool(
-        WorkPoolCreate(name="default-agent-pool", type="prefect-agent")
-    )
+    try:
+        return await prefect_client.create_work_pool(
+            WorkPoolCreate(name="default-agent-pool", type="prefect-agent")
+        )
+    except ObjectAlreadyExists:
+        return await prefect_client.read_work_pool("default-agent-pool")
 
 
 @pytest.fixture
@@ -184,6 +193,18 @@ def new_prompt(message, password=False, **kwargs):
     monkeypatch.setattr("prefect.cli._prompts.prompt", new_prompt)
 
 
+@pytest.fixture
+def mock_provide_password(monkeypatch):
+    def new_prompt(message, password=False, **kwargs):
+        if password:
+            return "my-token"
+        else:
+            return original_prompt(message, password=password, **kwargs)
+
+    original_prompt = prefect.cli._prompts.prompt
+    monkeypatch.setattr("prefect.cli.deploy.prompt", new_prompt)
+
+
 @pytest.fixture
 def mock_build_docker_image(monkeypatch):
     mock_build = mock.MagicMock()
@@ -197,6 +218,38 @@ def mock_build_docker_image(monkeypatch):
     return mock_build
 
 
+@pytest.fixture
+async def aws_credentials(prefect_client):
+    aws_credentials_type = await prefect_client.create_block_type(
+        block_type=BlockTypeCreate(
+            name="AWS Credentials",
+            slug="aws-credentials",
+        )
+    )
+
+    aws_credentials_schema = await prefect_client.create_block_schema(
+        block_schema=BlockSchemaCreate(
+            block_type_id=aws_credentials_type.id,
+            fields={"properties": {"aws_access_key_id": {"type": "string"}}},
+        )
+    )
+
+    return await prefect_client.create_block_document(
+        block_document=BlockDocumentCreate(
+            name="bezos-creds",
+            block_type_id=aws_credentials_type.id,
+            block_schema_id=aws_credentials_schema.id,
+            data={"aws_access_key_id": "AKIA1234"},
+        )
+    )
+
+
+@pytest.fixture
+def set_ui_url():
+    with temporary_settings({PREFECT_UI_URL: "http://gimmedata.com"}):
+        yield
+
+
 class TestProjectDeploySingleDeploymentYAML:
     """
     Tests for projects where deployment.yaml contains only one deployment
@@ -565,11 +618,16 @@ async def test_migrate_from_deployment_yaml_to_prefect_yaml(self, work_pool):
         await run_sync_in_worker_thread(
             invoke_and_assert,
             command="deploy",
-            user_input="y"
-            + readchar.key.ENTER
-            + readchar.key.ENTER
-            + "n"
-            + readchar.key.ENTER,
+            user_input=(
+                "y"
+                + readchar.key.ENTER
+                + readchar.key.ENTER
+                + "n"
+                + readchar.key.ENTER
+                # decline remote storage
+                + "n"
+                + readchar.key.ENTER
+            ),
             expected_code=0,
         )
 
@@ -584,6 +642,27 @@ async def test_migrate_from_deployment_yaml_to_prefect_yaml(self, work_pool):
 
 
 class TestProjectDeploy:
+    @pytest.fixture
+    def uninitialized_project_dir(self, project_dir):
+        Path(project_dir, "prefect.yaml").unlink()
+        return project_dir
+
+    @pytest.fixture
+    def uninitialized_project_dir_with_git_no_remote(self, uninitialized_project_dir):
+        subprocess.run(["git", "init"], cwd=uninitialized_project_dir)
+        assert Path(uninitialized_project_dir, ".git").exists()
+        return uninitialized_project_dir
+
+    @pytest.fixture
+    def uninitialized_project_dir_with_git_with_remote(
+        self, uninitialized_project_dir_with_git_no_remote
+    ):
+        subprocess.run(
+            ["git", "remote", "add", "origin", "https://example.com/org/repo.git"],
+            cwd=uninitialized_project_dir_with_git_no_remote,
+        )
+        return uninitialized_project_dir_with_git_no_remote
+
     async def test_project_deploy(self, project_dir, prefect_client):
         await prefect_client.create_work_pool(
             WorkPoolCreate(name="test-pool", type="test")
@@ -751,7 +830,10 @@ async def test_project_deploy_with_no_prefect_yaml_git_repo(
                 ),
                 expected_code=0,
                 user_input=(
-                    # Accept pulling from remote git origin
+                    # Accept pulling from remote storage
+                    readchar.key.ENTER
+                    +
+                    # Select remote Git repo as storage (first option)
                     readchar.key.ENTER
                     +
                     # Accept discovered URL
@@ -768,8 +850,8 @@ async def test_project_deploy_with_no_prefect_yaml_git_repo(
                     + readchar.key.ENTER
                 ),
                 expected_output_contains=[
-                    "Would you like your workers to pull your flow code from its remote"
-                    " repository when running this flow?"
+                    "Would you like your workers to pull your flow code from a remote"
+                    " storage location when running this flow?"
                 ],
             )
 
@@ -810,7 +892,10 @@ async def test_project_deploy_with_no_prefect_yaml_git_repo_user_overrides(
                 ),
                 expected_code=0,
                 user_input=(
-                    # Accept pulling from remote git origin
+                    # Accept pulling from remote storage
+                    readchar.key.ENTER
+                    +
+                    # Select remote Git repo as storage (first option)
                     readchar.key.ENTER
                     +
                     # Reject discovered URL
@@ -837,8 +922,8 @@ async def test_project_deploy_with_no_prefect_yaml_git_repo_user_overrides(
                     + readchar.key.ENTER
                 ),
                 expected_output_contains=[
-                    "Would you like your workers to pull your flow code from its remote"
-                    " repository when running this flow?"
+                    "Would you like your workers to pull your flow code from a remote"
+                    " storage location when running this flow?"
                 ],
             )
 
@@ -855,10 +940,14 @@ async def test_project_deploy_with_no_prefect_yaml_git_repo_user_overrides(
             ]
 
         @pytest.mark.usefixtures(
-            "interactive_console", "uninitialized_project_dir_with_git_with_remote"
+            "interactive_console",
+            "uninitialized_project_dir_with_git_with_remote",
+            "mock_provide_password",
         )
         async def test_project_deploy_with_no_prefect_yaml_git_repo_with_token(
-            self, work_pool, prefect_client
+            self,
+            work_pool,
+            prefect_client,
         ):
             await run_sync_in_worker_thread(
                 invoke_and_assert,
@@ -869,7 +958,10 @@ async def test_project_deploy_with_no_prefect_yaml_git_repo_with_token(
                 ),
                 expected_code=0,
                 user_input=(
-                    # Accept pulling from remote git origin
+                    # Accept pulling from remote storage
+                    readchar.key.ENTER
+                    +
+                    # Select remote Git repo as storage (first option)
                     readchar.key.ENTER
                     +
                     # Accept discovered URL
@@ -889,8 +981,8 @@ async def test_project_deploy_with_no_prefect_yaml_git_repo_with_token(
                     + readchar.key.ENTER
                 ),
                 expected_output_contains=[
-                    "Would you like your workers to pull your flow code from its remote"
-                    " repository when running this flow?"
+                    "Would you like your workers to pull your flow code from a remote"
+                    " storage location when running this flow?"
                 ],
             )
 
@@ -914,6 +1006,135 @@ async def test_project_deploy_with_no_prefect_yaml_git_repo_with_token(
             )
             assert token_block.get() == "my-token"
 
+        @pytest.mark.usefixtures("interactive_console", "uninitialized_project_dir")
+        async def test_deploy_with_blob_storage_select_existing_credentials(
+            self,
+            work_pool,
+            prefect_client,
+            aws_credentials,
+            monkeypatch,
+        ):
+            mock_step = mock.MagicMock()
+            monkeypatch.setattr(
+                "prefect.deployments.steps.core.import_object", lambda x: mock_step
+            )
+
+            await run_sync_in_worker_thread(
+                invoke_and_assert,
+                command=(
+                    "deploy ./flows/hello.py:my_flow -n test-name -p"
+                    f" {work_pool.name} --version 1.0.0 -v env=prod -t foo-bar"
+                    " --interval 60"
+                ),
+                expected_code=0,
+                user_input=(
+                    # Accept pulling from remote storage
+                    readchar.key.ENTER
+                    # Select S3 bucket as storage (second option)
+                    + readchar.key.DOWN
+                    + readchar.key.ENTER
+                    # Provide bucket name
+                    + "my-bucket"
+                    + readchar.key.ENTER
+                    # Accept default folder (root of bucket)
+                    + readchar.key.ENTER
+                    # Select existing credentials (first option)
+                    + readchar.key.ENTER
+                    # Decline saving the deployment configuration
+                    + "n"
+                    + readchar.key.ENTER
+                ),
+                expected_output_contains=[
+                    "Would you like your workers to pull your flow code from a remote"
+                    " storage location when running this flow?"
+                ],
+            )
+
+            deployment = await prefect_client.read_deployment_by_name(
+                "An important name/test-name"
+            )
+
+            assert deployment.pull_steps == [
+                {
+                    "prefect_aws.deployments.steps.pull_from_s3": {
+                        "bucket": "my-bucket",
+                        "folder": "",
+                        "credentials": (
+                            "{{ prefect.blocks.aws-credentials.bezos-creds }}"
+                        ),
+                    }
+                }
+            ]
+
+        @pytest.mark.usefixtures("interactive_console", "uninitialized_project_dir")
+        async def test_deploy_with_blob_storage_create_credentials(
+            self,
+            work_pool,
+            prefect_client,
+            aws_credentials,
+            set_ui_url,
+            monkeypatch,
+        ):
+            mock_step = mock.MagicMock()
+            monkeypatch.setattr(
+                "prefect.deployments.steps.core.import_object", lambda x: mock_step
+            )
+            await run_sync_in_worker_thread(
+                invoke_and_assert,
+                command=(
+                    "deploy ./flows/hello.py:my_flow -n test-name -p"
+                    f" {work_pool.name} --version 1.0.0 -v env=prod -t foo-bar"
+                    " --interval 60"
+                ),
+                expected_code=0,
+                user_input=(
+                    # Accept pulling from remote storage
+                    readchar.key.ENTER
+                    # Select S3 bucket as storage (first option)
+                    + readchar.key.ENTER
+                    # Provide bucket name
+                    + "my-bucket"
+                    + readchar.key.ENTER
+                    # Accept default folder (root of bucket)
+                    + readchar.key.ENTER
+                    # Create new credentials (second option)
+                    + readchar.key.DOWN
+                    + readchar.key.ENTER
+                    # Enter access key id (only field in this hypothetical)
+                    + "my-access-key-id"
+                    + readchar.key.ENTER
+                    # Accept default name for new credentials block (s3-storage-credentials)
+                    + readchar.key.ENTER
+                    # Accept saving the deployment configuration
+                    + "y"
+                    + readchar.key.ENTER
+                ),
+                expected_output_contains=[
+                    (
+                        "Would you like your workers to pull your flow code from a"
+                        " remote storage location when running this flow?"
+                    ),
+                    "View/Edit your new credentials block in the UI:",
+                    PREFECT_UI_URL.value(),
+                ],
+            )
+
+            deployment = await prefect_client.read_deployment_by_name(
+                "An important name/test-name"
+            )
+
+            assert deployment.pull_steps == [
+                {
+                    "prefect_aws.deployments.steps.pull_from_s3": {
+                        "bucket": "my-bucket",
+                        "folder": "",
+                        "credentials": (
+                            "{{ prefect.blocks.aws-credentials.s3-storage-credentials }}"
+                        ),
+                    }
+                }
+            ]
+
         @pytest.mark.usefixtures("interactive_console", "uninitialized_project_dir")
         async def test_build_docker_image_step_auto_build_dockerfile(
             self,
@@ -951,6 +1172,10 @@ async def test_build_docker_image_step_auto_build_dockerfile(
                 ),
                 expected_code=0,
                 user_input=(
+                    # Decline pulling from remote storage
+                    "n"
+                    + readchar.key.ENTER
+                    +
                     # Accept saving the deployment configuration
                     "y"
                     + readchar.key.ENTER
@@ -1027,9 +1252,11 @@ async def test_build_docker_image_step_custom_dockerfile_remote_flow_code_confir
                 ),
                 expected_code=0,
                 user_input=(
-                    # Accept pulling from remote git origin
-                    "y"
-                    + readchar.key.ENTER
+                    # Accept pulling from remote storage
+                    readchar.key.ENTER
+                    +
+                    # Select remote Git repo as storage (first option)
+                    readchar.key.ENTER
                     +
                     # Accept discovered URL
                     readchar.key.ENTER
@@ -1046,8 +1273,8 @@ async def test_build_docker_image_step_custom_dockerfile_remote_flow_code_confir
                 ),
                 expected_output_contains=[
                     (
-                        "Would you like to pull your flow code from its remote"
-                        " repository when running"
+                        "Would you like your workers to pull your flow code from a"
+                        " remote storage location when running this flow?"
                     ),
                     "Is this a private repository?",
                     "prefect deployment run 'An important name/test-name'",
@@ -1138,8 +1365,8 @@ async def test_build_docker_image_step_custom_dockerfile_remote_flow_code_reject
                 ),
                 expected_output_contains=[
                     (
-                        "Would you like to pull your flow code from its remote"
-                        " repository when running"
+                        "Would you like your workers to pull your flow code from a"
+                        " remote storage location when running this flow?"
                     ),
                     (
                         "Does your Dockerfile have a line that copies the current"
@@ -1225,8 +1452,8 @@ async def test_build_docker_image_step_custom_dockerfile_reject_copy_confirm(
                 ),
                 expected_output_contains=[
                     (
-                        "Would you like to pull your flow code from its remote"
-                        " repository when running"
+                        "Would you like your workers to pull your flow code from a"
+                        " remote storage location when running this flow?"
                     ),
                     (
                         "Does your Dockerfile have a line that copies the current"
@@ -1243,6 +1470,89 @@ async def test_build_docker_image_step_custom_dockerfile_reject_copy_confirm(
             with pytest.raises(ImportError):
                 import prefect_docker  # noqa
 
+    class TestGeneratedPushAction:
+        @pytest.mark.usefixtures(
+            "interactive_console", "uninitialized_project_dir_with_git_with_remote"
+        )
+        async def test_deploy_select_blob_storage_configures_push_step(
+            self,
+            work_pool,
+            prefect_client,
+            aws_credentials,
+            monkeypatch,
+        ):
+            mock_step = mock.MagicMock()
+            monkeypatch.setattr(
+                "prefect.deployments.steps.core.import_object", lambda x: mock_step
+            )
+
+            await run_sync_in_worker_thread(
+                invoke_and_assert,
+                command=(
+                    "deploy ./flows/hello.py:my_flow -n test-name"
+                    f" -p {work_pool.name} --interval 60"
+                ),
+                expected_code=0,
+                user_input=(
+                    # Accept pulling from remote storage
+                    "y"
+                    + readchar.key.ENTER
+                    # Select S3 bucket as storage (second option)
+                    + readchar.key.DOWN
+                    + readchar.key.ENTER
+                    # Provide bucket name
+                    + "my-bucket"
+                    + readchar.key.ENTER
+                    # Accept default folder (root of bucket)
+                    + readchar.key.ENTER
+                    # Select existing credentials (first option)
+                    + readchar.key.ENTER
+                    # Accept saving the deployment configuration
+                    + "y"
+                    + readchar.key.ENTER
+                ),
+                expected_output_contains=[
+                    "Would you like your workers to pull your flow code from a remote"
+                    " storage location when running this flow?"
+                ],
+            )
+
+            mock_step.assert_called_once_with(
+                bucket="my-bucket",
+                folder="",
+                credentials={"aws_access_key_id": "AKIA1234"},
+            )
+
+            prefect_file = Path("prefect.yaml")
+            assert prefect_file.exists()
+
+            with open(prefect_file, "r") as f:
+                config = yaml.safe_load(f)
+
+            assert config["push"] == [
+                {
+                    "prefect_aws.deployments.steps.push_to_s3": {
+                        "bucket": "my-bucket",
+                        "folder": "",
+                        "credentials": (
+                            "{{ prefect.blocks.aws-credentials.bezos-creds }}"
+                        ),
+                    }
+                }
+            ]
+
+            assert config["pull"] == [
+                {
+                    "prefect_aws.deployments.steps.pull_from_s3": {
+                        "bucket": "my-bucket",
+                        "folder": "",
+                        "credentials": (
+                            "{{ prefect.blocks.aws-credentials.bezos-creds }}"
+                        ),
+                    }
+                }
+            ]
+
     async def test_project_deploy_with_empty_dep_file(
         self, project_dir, prefect_client, work_pool
     ):
@@ -1598,10 +1908,17 @@ async def test_deploy_without_name_interactive(self, work_pool, prefect_client):
                 f"deploy ./flows/hello.py:my_flow -p {work_pool.name} --interval 3600"
             ),
             expected_code=0,
-            user_input="test-prompt-name"
-            + readchar.key.ENTER
-            + "n"
-            + readchar.key.ENTER,
+            user_input=(
+                # Provide a deployment name
+                "test-prompt-name"
+                + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
+                # Decline saving the deployment configuration
+                + "n"
+                + readchar.key.ENTER
+            ),
             expected_output_contains=[
                 "Deployment name",
             ],
@@ -1634,7 +1951,16 @@ async def test_deploy_without_work_pool_interactive(
             invoke_and_assert,
             command="deploy ./flows/hello.py:my_flow -n test-name --interval 3600",
             expected_code=0,
-            user_input=readchar.key.ENTER + "n" + readchar.key.ENTER,
+            user_input=(
+                # Select only existing work pool
+                readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
+                # Decline saving the deployment configuration
+                + "n"
+                + readchar.key.ENTER
+            ),
             expected_output_contains=[
                 "Which work pool would you like to deploy this flow to?",
             ],
@@ -1676,7 +2002,16 @@ async def test_deploy_with_prefect_agent_work_pool_interactive(
                 f" {default_agent_pool.name} --interval 3600"
             ),
             expected_code=0,
-            user_input=readchar.key.ENTER + "n" + readchar.key.ENTER,
+            user_input=(
+                # Accept only existing work pool
+                readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
+                # Decline saving the deployment configuration
+                + "n"
+                + readchar.key.ENTER
+            ),
             expected_output_contains=[
                 (
                     "You've chosen a work pool with type 'prefect-agent' which cannot"
@@ -1702,7 +2037,14 @@ async def test_deploy_with_push_pool_no_worker_start_message(self, push_work_poo
                 f" {push_work_pool.name} --interval 3600"
             ),
             expected_code=0,
-            user_input=readchar.key.ENTER + "n" + readchar.key.ENTER,
+            user_input=(
+                # Decline remote storage
+                "n"
+                + readchar.key.ENTER
+                # Decline saving the deployment configuration
+                + "n"
+                + readchar.key.ENTER
+            ),
             expected_output_does_not_contain=[
                 f"$ prefect worker start --pool {push_work_pool.name!r}",
             ],
@@ -1717,14 +2059,15 @@ async def test_deploy_with_no_available_work_pool_interactive(self, prefect_clie
             user_input=(
                 # Accept creating a new work pool
                 readchar.key.ENTER
-                +
                 # Select the first work pool type
-                readchar.key.ENTER
-                +
+                + readchar.key.ENTER
                 # Enter a name for the new work pool
-                "test-created-via-deploy"
+                + "test-created-via-deploy"
                 + readchar.key.ENTER
-                # Decline save
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
+                # Decline save the deployment configuration
                 + "n"
                 + readchar.key.ENTER
             ),
@@ -1830,8 +2173,11 @@ async def test_migrate_from_deployment_yaml_to_prefect_yaml(self, work_pool):
                 +
                 # choose existing deployment configuration
                 readchar.key.ENTER
+                # decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 +
-                # accept save
+                # decline save
                 "n"
                 + readchar.key.ENTER
             ),
@@ -1894,9 +2240,12 @@ async def test_deploy_does_not_prompt_schedule_when_empty_schedule_prefect_yaml(
             invoke_and_assert,
             command="deploy -n test-name",
             user_input=(
-                # reject saving configuration
+                # Decline remote storage
                 "n"
                 + readchar.key.ENTER
+                # reject saving configuration
+                + "n"
+                + readchar.key.ENTER
             ),
             expected_code=0,
         )
@@ -1934,9 +2283,12 @@ async def test_deploy_does_not_prompt_build_docker_image_when_empty_build_action
             invoke_and_assert,
             command="deploy -n test-name",
             user_input=(
-                # reject saving configuration
+                # Decline remote storage
                 "n"
                 + readchar.key.ENTER
+                # reject saving configuration
+                + "n"
+                + readchar.key.ENTER
             ),
             expected_code=0,
             expected_output_does_not_contain="Would you like to build a Docker image?",
@@ -1946,6 +2298,88 @@ async def test_deploy_does_not_prompt_build_docker_image_when_empty_build_action
             "An important name/test-name"
         )
 
+    @pytest.mark.usefixtures("interactive_console")
+    class TestRemoteStoragePicklist:
+        @pytest.mark.usefixtures("uninitialized_project_dir_with_git_no_remote")
+        async def test_no_git_option_when_no_remote_url(
+            self, docker_work_pool, aws_credentials, monkeypatch
+        ):
+            mock_step = mock.MagicMock()
+            monkeypatch.setattr(
+                "prefect.deployments.steps.core.import_object", lambda x: mock_step
+            )
+
+            await run_sync_in_worker_thread(
+                invoke_and_assert,
+                command=(
+                    "deploy ./flows/hello.py:my_flow -n test-name --cron '0 4 * * *' -p"
+                    " test-docker-work-pool"
+                ),
+                expected_code=0,
+                expected_output_contains="s3",
+                expected_output_does_not_contain="Git Repo",
+                user_input=(
+                    # no custom image
+                    "n"
+                    + readchar.key.ENTER
+                    # Accept remote storage
+                    + "y"
+                    + readchar.key.ENTER
+                    # Select S3
+                    + readchar.key.ENTER
+                    # Enter bucket name
+                    + "test-bucket"
+                    + readchar.key.ENTER
+                    # Enter bucket prefix
+                    + readchar.key.ENTER
+                    # Select existing credentials
+                    + readchar.key.ENTER
+                    # Decline saving the deployment configuration
+                    + "n"
+                    + readchar.key.ENTER
+                ),
+            )
+
+        @pytest.mark.usefixtures("uninitialized_project_dir_with_git_with_remote")
+        async def test_git_option_present_when_remote_url(
+            self, docker_work_pool, monkeypatch
+        ):
+            mock_step = mock.MagicMock()
+            monkeypatch.setattr(
+                "prefect.deployments.steps.core.import_object", lambda x: mock_step
+            )
+
+            await run_sync_in_worker_thread(
+                invoke_and_assert,
+                command=(
+                    "deploy ./flows/hello.py:my_flow -n test-name --cron '0 4 * * *' -p"
+                    " test-docker-work-pool"
+                ),
+                expected_code=0,
+                expected_output_contains="Git Repo",
+                expected_output_does_not_contain="s3",
+                user_input=(
+                    # no custom image
+                    "n"
+                    + readchar.key.ENTER
+                    # Accept remote storage
+                    + "y"
+                    + readchar.key.ENTER
+                    # Select Git (first option)
+                    + readchar.key.ENTER
+                    # Confirm git url
+                    + readchar.key.ENTER
+                    # Confirm git branch
+                    + readchar.key.ENTER
+                    # Not a private repo
+                    + "n"
+                    + readchar.key.ENTER
+                    # Decline saving the deployment configuration
+                    + "n"
+                    + readchar.key.ENTER
+                ),
+            )
+
 
 class TestSchedules:
     @pytest.mark.usefixtures("project_dir")
@@ -2184,6 +2618,9 @@ async def test_deploy_interval_schedule_interactive(
                 # Enter valid interval
                 "42"
                 + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # Decline save
                 + "n"
                 + readchar.key.ENTER
@@ -2231,9 +2668,12 @@ async def test_deploy_cron_schedule_interactive(self, prefect_client, work_pool)
                 # Select default timezone
                 readchar.key.ENTER
                 +
-                # Decline save
+                # Decline remote storage
                 "n"
                 + readchar.key.ENTER
+                # Decline save
+                + "n"
+                + readchar.key.ENTER
             ),
             expected_code=0,
             expected_output_contains=[
@@ -2279,6 +2719,10 @@ async def test_deploy_rrule_schedule_interactive(self, prefect_client, work_pool
                 # Select default timezone
                 readchar.key.ENTER
                 +
+                # Decline remote storage
+                "n"
+                + readchar.key.ENTER
+                +
                 # Decline save
                 "n"
                 + readchar.key.ENTER
@@ -2305,6 +2749,9 @@ async def test_deploy_no_schedule_interactive(self, prefect_client, work_pool):
                 # Decline schedule creation
                 "n"
                 + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # Decline save
                 + "n"
                 + readchar.key.ENTER
@@ -2694,15 +3141,21 @@ async def test_deploy_without_name_in_prefect_yaml_interactive(
             command="deploy --all",
             expected_code=0,
             user_input=(
-                # reject saving configuration
+                # decline remote storage
                 "n"
                 + readchar.key.ENTER
+                # reject saving configuration
+                + "n"
+                + readchar.key.ENTER
                 # accept naming deployment
                 + "y"
                 + readchar.key.ENTER
                 # enter deployment name
                 + "test-name-2"
                 + readchar.key.ENTER
+                # decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # reject saving configuration
                 + "n"
                 + readchar.key.ENTER
@@ -2751,9 +3204,12 @@ async def test_deploy_without_name_in_prefect_yaml_interactive_user_skips(
             command="deploy --all",
             expected_code=0,
             user_input=(
-                # reject saving configuration
+                # decline remote storage
                 "n"
                 + readchar.key.ENTER
+                # reject saving configuration
+                + "n"
+                + readchar.key.ENTER
                 # reject naming deployment
                 + "n"
                 + readchar.key.ENTER
@@ -3488,6 +3944,9 @@ async def test_deploy_with_two_deployments_with_same_name_interactive_prompts_se
                 # reject saving configuration
                 + "n"
                 + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
             ),
             expected_code=0,
             expected_output_contains=[
@@ -3619,7 +4078,15 @@ async def test_deploy_select_from_existing_deployments(
             invoke_and_assert,
             command="deploy",
             expected_code=0,
-            user_input=readchar.key.ENTER + "n" + readchar.key.ENTER,
+            user_input=(
+                readchar.key.ENTER
+                # decline remote storage
+                + "n"
+                + readchar.key.ENTER
+                # reject saving configuration
+                + "n"
+                + readchar.key.ENTER
+            ),
             expected_output_contains=[
                 "Would you like to use an existing deployment configuration?",
                 "test-name-1",
@@ -3675,6 +4142,9 @@ async def test_migrate_from_deployment_yaml_to_prefect_yaml(self, work_pool):
                 # accept migration
                 "y"
                 + readchar.key.ENTER
+                # decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # reject saving configuration
                 + "n"
                 + readchar.key.ENTER
@@ -3728,9 +4198,11 @@ def test_save_user_inputs_no_existing_prefect_file(self):
                 # enter work pool name
                 "inflatable"
                 + readchar.key.ENTER
-                +
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # accept save user inputs
-                "y"
+                + "y"
                 + readchar.key.ENTER
             ),
             expected_code=0,
@@ -3776,9 +4248,11 @@ def test_save_user_inputs_existing_prefect_file(self):
                 # enter work pool name
                 "inflatable"
                 + readchar.key.ENTER
-                +
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # accept save user inputs
-                "y"
+                + "y"
                 + readchar.key.ENTER
             ),
             expected_code=0,
@@ -3826,9 +4300,11 @@ def test_save_user_inputs_with_interval_schedule(self):
                 # enter work pool name
                 "inflatable"
                 + readchar.key.ENTER
-                +
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # accept save user inputs
-                "y"
+                + "y"
                 + readchar.key.ENTER
             ),
             expected_code=0,
@@ -3882,9 +4358,11 @@ def test_save_user_inputs_with_cron_schedule(self):
                 # enter work pool name
                 "inflatable"
                 + readchar.key.ENTER
-                +
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # accept save user inputs
-                "y"
+                + "y"
                 + readchar.key.ENTER
             ),
             expected_code=0,
@@ -3937,9 +4415,11 @@ def test_deploy_existing_deployment_with_no_changes_does_not_prompt_save(self):
                 # enter work pool name
                 "inflatable"
                 + readchar.key.ENTER
-                +
+                # decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # accept save user inputs
-                "y"
+                + "y"
                 + readchar.key.ENTER
             ),
             expected_code=0,
@@ -3970,14 +4450,15 @@ def test_deploy_existing_deployment_with_no_changes_does_not_prompt_save(self):
         invoke_and_assert(
             command="deploy -n existing-deployment --cron '* * * * *'",
             user_input=(
+                # decline remote storage
+                "n"
+                + readchar.key.ENTER
                 # accept create work pool
-                readchar.key.ENTER
-                +
+                + readchar.key.ENTER
                 # choose process work pool
-                readchar.key.ENTER
-                +
+                + readchar.key.ENTER
                 # enter work pool name
-                "inflatable"
+                + "inflatable"
                 + readchar.key.ENTER
             ),
             expected_code=0,
@@ -4025,9 +4506,11 @@ def test_deploy_existing_deployment_with_changes_prompts_save(self):
                 # enter work pool name
                 "inflatable"
                 + readchar.key.ENTER
-                +
+                # decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # accept save user inputs
-                "y"
+                + "y"
                 + readchar.key.ENTER
             ),
             expected_code=0,
@@ -4054,18 +4537,19 @@ def test_deploy_existing_deployment_with_changes_prompts_save(self):
         invoke_and_assert(
             command="deploy -n existing-deployment --cron '* * * * *'",
             user_input=(
+                # decline remote storage
+                "n"
+                + readchar.key.ENTER
                 # accept create work pool
-                readchar.key.ENTER
-                +
+                + readchar.key.ENTER
                 # choose process work pool
-                readchar.key.ENTER
+                + readchar.key.ENTER
                 +
                 # enter work pool name
                 "inflatable"
                 + readchar.key.ENTER
-                +
                 # accept save user inputs
-                "y"
+                + "y"
                 + readchar.key.ENTER
                 # accept found existing deployment
                 + "y"
@@ -4122,9 +4606,11 @@ def test_save_user_inputs_with_rrule_schedule(self):
                 # enter work pool name
                 "inflatable"
                 + readchar.key.ENTER
-                +
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # accept save user inputs
-                "y"
+                + "y"
                 + readchar.key.ENTER
             ),
             expected_code=0,
@@ -4267,23 +4753,21 @@ def test_save_user_inputs_overwrite_confirmed(self):
             user_input=(
                 # Accept default deployment name
                 readchar.key.ENTER
-                +
                 # decline schedule
-                "n"
+                + "n"
                 + readchar.key.ENTER
-                +
                 # accept create work pool
-                readchar.key.ENTER
-                +
+                + readchar.key.ENTER
                 # choose process work pool
-                readchar.key.ENTER
-                +
+                + readchar.key.ENTER
                 # enter work pool name
-                "inflatable"
+                + "inflatable"
+                + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
                 + readchar.key.ENTER
-                +
                 # accept save user inputs
-                "y"
+                + "y"
                 + readchar.key.ENTER
             ),
             expected_code=0,
@@ -4324,6 +4808,9 @@ def test_save_user_inputs_overwrite_confirmed(self):
                 +
                 # accept create work pool
                 readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 +
                 # choose process work pool
                 readchar.key.ENTER
@@ -4331,13 +4818,11 @@ def test_save_user_inputs_overwrite_confirmed(self):
                 # enter work pool name
                 "inflatable"
                 + readchar.key.ENTER
-                +
                 # accept save user inputs
-                "y"
+                + "y"
                 + readchar.key.ENTER
-                +
                 # accept overwriting existing deployment that is found
-                "y"
+                + "y"
                 + readchar.key.ENTER
             ),
             expected_code=0,
@@ -4376,9 +4861,11 @@ def test_save_user_inputs_overwrite_rejected_saving_cancelled(self):
                 # enter work pool name
                 "inflatable"
                 + readchar.key.ENTER
-                +
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # accept save user inputs
-                "y"
+                + "y"
                 + readchar.key.ENTER
             ),
             expected_code=0,
@@ -4406,33 +4893,28 @@ def test_save_user_inputs_overwrite_rejected_saving_cancelled(self):
                 # configure new deployment
                 "n"
                 + readchar.key.ENTER
-                +
                 # accept schedule
-                readchar.key.ENTER
-                +
+                + readchar.key.ENTER
                 # select interval schedule
-                readchar.key.ENTER
-                +
+                + readchar.key.ENTER
                 # enter interval schedule
-                "3600"
+                + "3600"
                 + readchar.key.ENTER
-                +
                 # accept create work pool
-                readchar.key.ENTER
-                +
+                + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # choose process work pool
-                readchar.key.ENTER
-                +
+                + readchar.key.ENTER
                 # enter work pool name
-                "inflatable"
+                + "inflatable"
                 + readchar.key.ENTER
-                +
                 # accept save user inputs
-                "y"
+                + "y"
                 + readchar.key.ENTER
-                +
                 # reject overwriting existing deployment that is found
-                "n"
+                + "n"
                 + readchar.key.ENTER
             ),
             expected_code=0,
@@ -4495,9 +4977,11 @@ async def test_deploy_resolves_variables_in_deployments_section(
                 # reject schedule
                 "n"
                 + readchar.key.ENTER
-                +
+                # decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # accept saving configuration
-                "y"
+                + "y"
                 + readchar.key.ENTER
                 # accept overwrite config
                 + "y"
@@ -4550,6 +5034,10 @@ async def test_deploy_without_entrypoint(self, prefect_client: PrefectClient):
                 # accept first work pool
                 readchar.key.ENTER
                 +
+                # Decline remote storage
+                "n"
+                + readchar.key.ENTER
+                +
                 # decline save user inputs
                 "n"
                 + readchar.key.ENTER
@@ -4594,6 +5082,10 @@ async def test_deploy_without_entrypoint_manually_enter(
                 # accept first work pool
                 readchar.key.ENTER
                 +
+                # Decline remote storage
+                "n"
+                + readchar.key.ENTER
+                +
                 # decline save user inputs
                 "n"
                 + readchar.key.ENTER
@@ -4647,6 +5139,10 @@ async def test_deploy_validates_manually_entered_entrypoints(
                 # accept first work pool
                 readchar.key.ENTER
                 +
+                # Decline remote storage
+                "n"
+                + readchar.key.ENTER
+                +
                 # decline save user inputs
                 "n"
                 + readchar.key.ENTER
@@ -4689,6 +5185,10 @@ async def test_deploy_without_entrypoint_no_flows_found(
                 # accept first work pool
                 readchar.key.ENTER
                 +
+                # Decline remote storage
+                "n"
+                + readchar.key.ENTER
+                +
                 # decline save user inputs
                 "n"
                 + readchar.key.ENTER
@@ -5286,6 +5786,9 @@ async def test_triggers_saved_to_prefect_yaml(self, docker_work_pool):
                         # Decline docker build
                         + "n"
                         + readchar.key.ENTER
+                        # Decline remote storage
+                        + "n"
+                        + readchar.key.ENTER
                         # Accept save configuration
                         + "y"
                         + readchar.key.ENTER
@@ -5340,6 +5843,10 @@ async def test_docker_build_step_exists_does_not_prompt_build_custom_docker_imag
                 f" {docker_work_pool.name}"
             ),
             user_input=(
+                # Decline remote storage
+                "n"
+                + readchar.key.ENTER
+                +
                 # Accept save configuration
                 "y"
                 + readchar.key.ENTER
@@ -5386,6 +5893,9 @@ async def test_other_build_step_exists_prompts_build_custom_docker_image(
                 # Reject build custom docker image
                 "n"
                 + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # Accept save configuration
                 + "y"
                 + readchar.key.ENTER
@@ -5420,6 +5930,9 @@ async def test_no_build_step_exists_prompts_build_custom_docker_image(
                 # Reject build custom docker image
                 "n"
                 + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # Accept save configuration
                 + "y"
                 + readchar.key.ENTER
@@ -5473,6 +5986,9 @@ async def test_prompt_build_custom_docker_image_accepted_use_existing_dockerfile
                 # Reject push to registry
                 "n"
                 + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # Accept save configuration
                 + "y"
                 + readchar.key.ENTER
@@ -5545,6 +6061,9 @@ async def test_prompt_build_custom_docker_image_accepted_use_existing_dockerfile
                 # Reject push to registry
                 "n"
                 + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # Accept save configuration
                 + "y"
                 + readchar.key.ENTER
@@ -5640,6 +6159,9 @@ async def test_prompt_build_custom_docker_image_accepted_no_existing_dockerfile_
                 # Reject push to registry
                 + "n"
                 + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # Accept save configuration
                 + "y"
                 + readchar.key.ENTER
@@ -5700,6 +6222,9 @@ async def test_no_existing_work_pool_image_gets_updated_after_adding_build_docke
                 # Reject push to registry
                 + "n"
                 + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # Accept save configuration
                 + "y"
                 + readchar.key.ENTER
@@ -5771,6 +6296,9 @@ async def test_work_pool_image_already_exists_not_updated_after_adding_build_doc
                 # Reject push to registry
                 + "n"
                 + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # Accept save configuration
                 + "y"
                 + readchar.key.ENTER
@@ -5902,6 +6430,9 @@ async def test_prompt_push_custom_docker_image_rejected(
                 # Reject push to registry
                 + "n"
                 + readchar.key.ENTER
+                # Decline remote storage
+                + "n"
+                + readchar.key.ENTER
                 # Accept save configuration
                 + "y"
                 + readchar.key.ENTER