Repository: PrefectHQ/prefect

Issue:
Allow writing deployment details to project config file after creating new deployment
After running through the `prefect deploy` wizard, prompt the user asking them if they want to save the configuration for the deployment they just created. If they accept, write the inputs provided by the CLI to a `deployment.yaml` file in the current directory, creating the file if it does not already exist.

This functionality should save new deployments to an existing `deployment.yaml` file by appending the new deployment config to the `deployment` list in the existing `deployment.yaml` file. Names of entries in the `deployments` list need to be unique.

Updating existing deployments via this functionality is out of scope for this issue.


Discussion:

Resolving patch:
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
@@ -43,6 +43,7 @@
     _copy_deployments_into_prefect_file,
     _get_git_branch,
     _get_git_remote_origin_url,
+    _save_deployment_to_prefect_file,
 )
 
 from prefect.blocks.system import Secret
@@ -182,6 +183,8 @@ async def deploy(
 
     Should be run from a project root directory.
     """
+    # TODO: This function is getting out of hand. It should be refactored into smaller
+    # functions.
 
     options = {
         "entrypoint": entrypoint,
@@ -200,7 +203,9 @@ async def deploy(
         "param": param,
         "params": params,
     }
-
+    # flag to track deployment.yaml format
+    # TODO: remove when deployment.yaml support is removed
+    multi_deployment_format = True
     try:
         with open("prefect.yaml", "r") as f:
             project = yaml.safe_load(f)
@@ -211,10 +216,12 @@ async def deploy(
         with open("deployment.yaml", "r") as f:
             base_deploy = yaml.safe_load(f)
             if not base_deploy:
-                deployments = []
+                multi_deployment_format = False
+                deployments = [{}]
             elif base_deploy.get("deployments"):
                 deployments = base_deploy["deployments"]
             else:
+                multi_deployment_format = False
                 deployments = [base_deploy]
         if is_interactive() and not ci and project:
             if confirm(
@@ -257,7 +264,7 @@ async def deploy(
     except FileNotFoundError:
         deployments = project.get("deployments", [])
     try:
-        if len(deployments) > 1:
+        if len(deployments) >= 1:
             if deploy_all or len(names) > 1:
                 if any(options.values()):
                     app.console.print(
@@ -275,16 +282,16 @@ async def deploy(
                     deploy_all=deploy_all,
                     ci=ci,
                 )
-            elif len(names) == 1:
+            elif len(names) == 1 and multi_deployment_format:
                 deployment = next(
                     (d for d in deployments if d.get("name") == names[0]), {}
                 )
                 if not deployment:
                     app.console.print(
                         (
-                            "Could not find deployment configuration with name "
-                            f"{names[0]!r}. Only CLI options "
-                            "will be used for this deployment."
+                            "Could not find deployment configuration with name"
+                            f" {names[0]!r}. Your flow will be deployed with a new"
+                            " deployment configuration."
                         ),
                         style="yellow",
                     )
@@ -292,49 +299,59 @@ async def deploy(
                 await _run_single_deploy(
                     base_deploy=deployment, project=project, options=options, ci=ci
                 )
+            elif not multi_deployment_format:
+                options["name"] = names[0] if len(names) == 1 else None
+                await _run_single_deploy(
+                    base_deploy=deployments[0],
+                    project=project,
+                    options=options,
+                    ci=ci,
+                )
             else:
                 if not is_interactive() or ci:
                     exit_with_error(
-                        "Discovered multiple deployment configurations,"
+                        "Discovered one or more deployment configurations,"
                         " but no name was given. Please specify the name of at least"
                         " one deployment to create or update."
                     )
-                selected_deployment = prompt_select_from_table(
-                    app.console,
-                    "Would you like to use an existing deployment configuration?",
-                    [
-                        {"header": "Name", "key": "name"},
-                        {"header": "Description", "key": "description"},
-                    ],
-                    [
-                        deployment
-                        for deployment in deployments
-                        if deployment.get("name")
-                    ],
-                    opt_out_message="No, configure a new deployment",
-                    opt_out_response={},
-                )
+                deployments = [
+                    deployment
+                    for deployment in deployments
+                    if deployment.get("name") and deployment.get("entrypoint")
+                ]
+                if deployments:
+                    selected_deployment = prompt_select_from_table(
+                        app.console,
+                        "Would you like to use an existing deployment configuration?",
+                        [
+                            {"header": "Name", "key": "name"},
+                            {"header": "Entrypoint", "key": "entrypoint"},
+                            {"header": "Description", "key": "description"},
+                        ],
+                        [
+                            deployment
+                            for deployment in deployments
+                            if deployment.get("name")
+                        ],
+                        opt_out_message="No, configure a new deployment",
+                        opt_out_response={},
+                    )
+                else:
+                    selected_deployment = {}
                 await _run_single_deploy(
                     base_deploy=selected_deployment,
                     project=project,
                     options=options,
                     ci=ci,
                 )
-        elif len(deployments) <= 1:
-            if len(names) > 1:
-                exit_with_error(
-                    "Multiple deployment names were provided, but only one deployment"
-                    " configuration was found. Please provide a single deployment"
-                    " name."
-                )
-            else:
-                options["name"] = names[0] if names else None
-                await _run_single_deploy(
-                    base_deploy=deployments[0] if deployments else {},
-                    project=project,
-                    options=options,
-                    ci=ci,
-                )
+        else:
+            options["name"] = names[0] if len(names) == 1 else None
+            await _run_single_deploy(
+                base_deploy={},
+                project=project,
+                options=options,
+                ci=ci,
+            )
     except ValueError as exc:
         exit_with_error(str(exc))
 
@@ -351,6 +368,7 @@ async def _run_single_deploy(
     project = deepcopy(project) if project else {}
     options = deepcopy(options) if options else {}
 
+    should_prompt_for_save = is_interactive() and not ci and not bool(base_deploy)
     base_deploy = _merge_with_default_deployment(base_deploy)
 
     base_deploy_schedule = base_deploy.get("schedule")
@@ -543,9 +561,10 @@ async def _run_single_deploy(
                 )
         except ObjectNotFound:
             raise ValueError(
-                "This deployment references a work pool that does not exist."
-                " This means no worker will be able to pick up its runs. You"
-                " can create a work pool in the Prefect UI."
+                "This deployment configuration references work pool"
+                f" {base_deploy['work_pool']['name']!r} which does not exist. This"
+                " means no worker will be able to pick up its runs. You can create a"
+                " work pool in the Prefect UI."
             )
     else:
         if not is_interactive() or ci:
@@ -634,7 +653,30 @@ async def _run_single_deploy(
     if PREFECT_UI_URL:
         app.console.print(
             "\nView Deployment in UI:"
-            f" {PREFECT_UI_URL.value()}/deployments/deployment/{deployment_id}"
+            f" {PREFECT_UI_URL.value()}/deployments/deployment/{deployment_id}\n"
+        )
+
+    if should_prompt_for_save and confirm(
+        (
+            "Would you like to save configuration for this deployment for faster"
+            " deployments in the future?"
+        ),
+        console=app.console,
+    ):
+        _save_deployment_to_prefect_file(
+            base_deploy,
+            build_steps=build_steps or None,
+            push_steps=push_steps or None,
+            pull_steps=pull_steps or None,
+        )
+        app.console.print(
+            (
+                "\n[green]Deployment configuration saved to prefect.yaml![/] You can"
+                " now deploy using this deployment configuration with:\n\n\t[blue]$"
+                f" prefect deploy -n {base_deploy['name']}[/]\n\nYou can also make"
+                " changes to this deployment configuration by making changes to the"
+                " prefect.yaml file."
+            ),
         )
 
     app.console.print(
@@ -686,7 +728,7 @@ async def _run_multi_deploy(
                     app.console.print("Skipping unnamed deployment.", style="yellow")
                     continue
             app.console.print(Panel(f"Deploying {base_deploy['name']}", style="blue"))
-            await _run_single_deploy(base_deploy, project)
+            await _run_single_deploy(base_deploy, project, ci=ci)
     else:
         picked_base_deploys = [
             base_deploy for base_deploy in base_deploys if base_deploy["name"] in names
@@ -705,7 +747,7 @@ async def _run_multi_deploy(
         app.console.print("Deploying selected deployments for current project...")
         for base_deploy in picked_base_deploys:
             app.console.print(Panel(f"Deploying {base_deploy['name']}", style="blue"))
-            await _run_single_deploy(base_deploy, project)
+            await _run_single_deploy(base_deploy, project, ci=ci)
 
 
 def _construct_schedule(
@@ -754,9 +796,6 @@ def _construct_schedule(
     return schedule
 
 
-DEFAULT_DEPLOYMENT = None
-
-
 def _merge_with_default_deployment(base_deploy: Dict):
     """
     Merge a base deployment dictionary with the default deployment dictionary.
@@ -771,18 +810,21 @@ def _merge_with_default_deployment(base_deploy: Dict):
         The merged deployment dictionary.
     """
     base_deploy = deepcopy(base_deploy)
-    global DEFAULT_DEPLOYMENT
-
-    if DEFAULT_DEPLOYMENT is None:
-        # load the default deployment file for key consistency
-        default_file = (
-            Path(__file__).parent.parent / "deployments" / "templates" / "prefect.yaml"
-        )
-
-        # load default file
-        with open(default_file, "r") as df:
-            contents = yaml.safe_load(df)
-            DEFAULT_DEPLOYMENT = contents["deployments"][0]
+    DEFAULT_DEPLOYMENT = {
+        "name": None,
+        "version": None,
+        "tags": [],
+        "description": None,
+        "schedule": {},
+        "flow_name": None,
+        "entrypoint": None,
+        "parameters": {},
+        "work_pool": {
+            "name": None,
+            "work_queue_name": None,
+            "job_variables": {},
+        },
+    }
 
     # merge default and base deployment
     # this allows for missing keys in a user's deployment file
diff --git a/src/prefect/deployments/base.py b/src/prefect/deployments/base.py
--- a/src/prefect/deployments/base.py
+++ b/src/prefect/deployments/base.py
@@ -4,20 +4,25 @@
 
 To get started, follow along with [the project tutorial](/tutorials/projects/).
 """
+from copy import deepcopy
 import json
 import os
 import subprocess
 import sys
 from pathlib import Path
-from typing import List, Optional
+from typing import Dict, List, Optional
+from pydantic import BaseModel
 
 import yaml
+from ruamel.yaml import YAML
 
 from prefect.flows import load_flow_from_entrypoint
 from prefect.utilities.asyncutils import run_sync_in_worker_thread
 from prefect.utilities.filesystem import create_default_ignore_file
 from prefect.utilities.templating import apply_values
 
+from prefect.client.schemas.schedules import IntervalSchedule
+
 
 def find_prefect_directory(path: Path = None) -> Optional[Path]:
     """
@@ -74,9 +79,8 @@ def create_default_prefect_yaml(
         return False
     default_file = Path(__file__).parent / "templates" / "prefect.yaml"
 
-    if contents is None:
-        with default_file.open(mode="r") as df:
-            contents = yaml.safe_load(df)
+    with default_file.open(mode="r") as df:
+        default_contents = yaml.safe_load(df)
 
     import prefect
 
@@ -86,7 +90,9 @@ def create_default_prefect_yaml(
     with prefect_file.open(mode="w") as f:
         # write header
         f.write(
-            "# File for configuring project / deployment build, push and pull steps\n\n"
+            "# Welcome to your prefect.yaml file! You can you this file for storing and"
+            " managing\n# configuration for deploying your flows. We recommend"
+            " committing this file to source\n# control along with your flow code.\n\n"
         )
 
         f.write("# Generic metadata about this project\n")
@@ -96,7 +102,11 @@ def create_default_prefect_yaml(
 
         # build
         f.write("# build section allows you to manage and build docker images\n")
-        yaml.dump({"build": contents["build"]}, f, sort_keys=False)
+        yaml.dump(
+            {"build": contents.get("build", default_contents.get("build"))},
+            f,
+            sort_keys=False,
+        )
         f.write("\n")
 
         # push
@@ -104,7 +114,11 @@ def create_default_prefect_yaml(
             "# push section allows you to manage if and how this project is uploaded to"
             " remote locations\n"
         )
-        yaml.dump({"push": contents["push"]}, f, sort_keys=False)
+        yaml.dump(
+            {"push": contents.get("push", default_contents.get("push"))},
+            f,
+            sort_keys=False,
+        )
         f.write("\n")
 
         # pull
@@ -112,7 +126,11 @@ def create_default_prefect_yaml(
             "# pull section allows you to provide instructions for cloning this project"
             " in remote locations\n"
         )
-        yaml.dump({"pull": contents["pull"]}, f, sort_keys=False)
+        yaml.dump(
+            {"pull": contents.get("pull", default_contents.get("pull"))},
+            f,
+            sort_keys=False,
+        )
         f.write("\n")
 
         # deployments
@@ -120,7 +138,15 @@ def create_default_prefect_yaml(
             "# the deployments section allows you to provide configuration for"
             " deploying flows\n"
         )
-        yaml.dump({"deployments": contents.get("deployments", [])}, f, sort_keys=False)
+        yaml.dump(
+            {
+                "deployments": contents.get(
+                    "deployments", default_contents.get("deployments")
+                )
+            },
+            f,
+            sort_keys=False,
+        )
     return True
 
 
@@ -339,3 +365,77 @@ def _copy_deployments_into_prefect_file():
         else:
             f.write("\n")
             f.write(raw_deployment_file_contents)
+
+
+def _save_deployment_to_prefect_file(
+    deployment: Dict,
+    build_steps: Optional[List[Dict]] = None,
+    push_steps: Optional[List[Dict]] = None,
+    pull_steps: Optional[List[Dict]] = None,
+):
+    """
+    Save a deployment configuration to the `prefect.yaml` file in the
+    current directory.
+
+    Will create a prefect.yaml file if one does not already exist.
+
+    Args:
+        - deployment: a dictionary containing a deployment configuration
+    """
+    if not deployment:
+        raise ValueError("Deployment must be a non-empty dictionary.")
+    deployment = deepcopy(deployment)
+    # Parameter schema is not stored in prefect.yaml
+    deployment.pop("parameter_openapi_schema")
+    # Only want entrypoint to avoid errors
+    deployment.pop("flow_name", None)
+
+    if deployment.get("schedule"):
+        if isinstance(deployment["schedule"], IntervalSchedule):
+            deployment["schedule"] = deployment["schedule"].dict()
+            deployment["schedule"]["interval"] = deployment["schedule"][
+                "interval"
+            ].total_seconds()
+            deployment["schedule"]["anchor_date"] = deployment["schedule"][
+                "anchor_date"
+            ].isoformat()
+        elif isinstance(deployment["schedule"], BaseModel):
+            deployment["schedule"] = deployment["schedule"].dict()
+
+    current_directory_name = os.path.basename(os.getcwd())
+    prefect_file = Path("prefect.yaml")
+    if not prefect_file.exists():
+        create_default_prefect_yaml(
+            ".",
+            current_directory_name,
+            contents={
+                "deployments": [deployment],
+                "build": build_steps,
+                "push": push_steps,
+                "pull": pull_steps,
+            },
+        )
+        create_default_ignore_file(".")
+    else:
+        # use ruamel.yaml to preserve comments
+        ryaml = YAML()
+        with prefect_file.open(mode="r") as f:
+            parsed_prefect_file_contents = ryaml.load(f)
+
+        if build_steps != parsed_prefect_file_contents.get("build"):
+            deployment["build"] = build_steps
+
+        if push_steps != parsed_prefect_file_contents.get("push"):
+            deployment["push"] = build_steps
+
+        if pull_steps != parsed_prefect_file_contents.get("pull"):
+            deployment["pull"] = build_steps
+
+        deployments = parsed_prefect_file_contents.get("deployments")
+        if deployments is None:
+            parsed_prefect_file_contents["deployments"] = [deployment]
+        else:
+            deployments.append(deployment)
+
+        with prefect_file.open(mode="w") as f:
+            ryaml.dump(parsed_prefect_file_contents, f)


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
@@ -476,7 +476,7 @@ 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,
+            user_input="y" + readchar.key.ENTER + "n" + readchar.key.ENTER,
             expected_code=0,
         )
 
@@ -606,7 +606,6 @@ async def test_project_deploy_generates_pull_action(
                 }
             ]
 
-        @pytest.mark.usefixtures("interactive_console")
         async def test_project_deploy_with_no_prefect_yaml_git_repo_no_remote(
             self,
             work_pool,
@@ -648,8 +647,9 @@ async def test_project_deploy_with_no_prefect_yaml_git_repo_user_rejects(
                     f" {work_pool.name} --version 1.0.0 -v env=prod -t foo-bar"
                     " --interval 60"
                 ),
-                # User rejects pulling from the remote repo
-                user_input="n" + readchar.key.ENTER,
+                # User rejects pulling from the remote repo and rejects saving the
+                # deployment configuration
+                user_input="n" + readchar.key.ENTER + "n" + readchar.key.ENTER,
                 expected_code=0,
             )
 
@@ -691,6 +691,9 @@ async def test_project_deploy_with_no_prefect_yaml_git_repo(
                     # Choose public repo
                     "n"
                     + 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 its remote"
@@ -710,6 +713,16 @@ async def test_project_deploy_with_no_prefect_yaml_git_repo(
                 }
             ]
 
+            prefect_file_contents = yaml.safe_load(Path("prefect.yaml").read_text())
+            assert prefect_file_contents["pull"] == [
+                {
+                    "prefect.deployments.steps.git_clone": {
+                        "repository": "https://example.com/org/repo.git",
+                        "branch": "main",
+                    }
+                }
+            ]
+
         @pytest.mark.usefixtures(
             "interactive_console", "uninitialized_project_dir_with_git_with_remote"
         )
@@ -747,6 +760,9 @@ async def test_project_deploy_with_no_prefect_yaml_git_repo_user_overrides(
                     # Choose public repo
                     "n"
                     + 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 its remote"
@@ -796,6 +812,9 @@ async def test_project_deploy_with_no_prefect_yaml_git_repo_with_token(
                     # Enter token
                     + "my-token"
                     + 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 its remote"
@@ -849,6 +868,7 @@ async def test_project_deploy_templates_values(self, work_pool, prefect_client):
         with prefect_file.open(mode="r") as f:
             contents = yaml.safe_load(f)
 
+        contents["deployments"][0]["name"] = "test-name"
         contents["deployments"][0]["version"] = "{{ input }}"
         contents["deployments"][0]["tags"] = "{{ output2 }}"
         contents["deployments"][0]["description"] = "{{ output1 }}"
@@ -908,7 +928,7 @@ async def test_project_deploy_with_default_parameters(
 
         await run_sync_in_worker_thread(
             invoke_and_assert,
-            command="deploy",
+            command="deploy -n test-name",
             expected_code=0,
             expected_output_contains="An important name/test-name",
         )
@@ -941,7 +961,7 @@ async def test_project_deploy_with_default_parameters_from_cli(
 
         await run_sync_in_worker_thread(
             invoke_and_assert,
-            command=f"deploy {option}",
+            command=f"deploy -n test-name {option}",
             expected_code=0,
             expected_output_contains="An important name/test-name",
         )
@@ -1020,7 +1040,7 @@ async def test_project_deploy_reads_flow_name_from_prefect_yaml(self, work_pool)
 
         await run_sync_in_worker_thread(
             invoke_and_assert,
-            command="deploy",
+            command="deploy -n test-name",
             expected_code=0,
             expected_output_contains="An important name/test-name",
         )
@@ -1040,7 +1060,7 @@ async def test_project_deploy_reads_entrypoint_from_prefect_yaml(self, work_pool
 
         await run_sync_in_worker_thread(
             invoke_and_assert,
-            command="deploy",
+            command="deploy -n test-name",
             expected_code=0,
             expected_output_contains="An important name/test-name",
         )
@@ -1061,7 +1081,7 @@ async def test_project_deploy_exits_with_name_and_entrypoint_passed(
 
         await run_sync_in_worker_thread(
             invoke_and_assert,
-            command="deploy -f 'An important name' flows/hello.py:my_flow",
+            command="deploy -f 'An important name' -n test-name flows/hello.py:my_flow",
             expected_code=1,
             expected_output=(
                 "Received an entrypoint and a flow name for this deployment. Please"
@@ -1085,7 +1105,7 @@ async def test_project_deploy_exits_with_no_name_or_entrypoint_configured(
 
         await run_sync_in_worker_thread(
             invoke_and_assert,
-            command="deploy",
+            command="deploy -n test-name",
             expected_code=1,
             expected_output_contains="An entrypoint or flow name must be provided.",
         )
@@ -1098,7 +1118,10 @@ 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,
+            user_input="test-prompt-name"
+            + readchar.key.ENTER
+            + "n"
+            + readchar.key.ENTER,
             expected_output_contains=[
                 "Deployment name",
             ],
@@ -1131,7 +1154,7 @@ 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,
+            user_input=readchar.key.ENTER + "n" + readchar.key.ENTER,
             expected_output_contains=[
                 "Which work pool would you like to deploy this flow to?",
             ],
@@ -1155,7 +1178,7 @@ async def test_deploy_with_prefect_agent_work_pool_non_interactive(
                 f" {default_agent_pool.name}"
             ),
             expected_code=1,
-            expected_output=(
+            expected_output_contains=(
                 "Cannot create a project-style deployment with work pool of type"
                 " 'prefect-agent'. If you wish to use an agent with your deployment,"
                 " please use the `prefect deployment build` command."
@@ -1173,7 +1196,7 @@ 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,
+            user_input=readchar.key.ENTER + "n" + readchar.key.ENTER,
             expected_output_contains=[
                 (
                     "You've chosen a work pool with type 'prefect-agent' which cannot"
@@ -1208,6 +1231,9 @@ async def test_deploy_with_no_available_work_pool_interactive(
                 # Enter a name for the new work pool
                 "test-created-via-deploy"
                 + readchar.key.ENTER
+                # Decline save
+                + "n"
+                + readchar.key.ENTER
             ),
             expected_output_contains=[
                 (
@@ -1304,8 +1330,18 @@ async def test_migrate_from_deployment_yaml_to_prefect_yaml(self, work_pool):
         await run_sync_in_worker_thread(
             invoke_and_assert,
             command="deploy",
-            # accept migration
-            user_input="y" + readchar.key.ENTER,
+            user_input=(
+                # accept migration
+                "y"
+                + readchar.key.ENTER
+                +
+                # choose existing deployment configuration
+                readchar.key.ENTER
+                +
+                # accept save
+                "n"
+                + readchar.key.ENTER
+            ),
             expected_code=0,
             expected_output_contains=[
                 "Successfully copied your deployment configurations into your"
@@ -1349,6 +1385,7 @@ async def test_deployment_yaml_cron_schedule(self, work_pool, prefect_client):
         with prefect_file.open(mode="r") as f:
             deploy_config = yaml.safe_load(f)
 
+        deploy_config["deployments"][0]["name"] = "test-name"
         deploy_config["deployments"][0]["schedule"]["cron"] = "0 4 * * *"
         deploy_config["deployments"][0]["schedule"]["timezone"] = "America/Chicago"
 
@@ -1377,6 +1414,7 @@ async def test_deployment_yaml_cron_schedule_timezone_cli(
         with prefect_file.open(mode="r") as f:
             deploy_config = yaml.safe_load(f)
 
+        deploy_config["deployments"][0]["name"] = "test-name"
         deploy_config["deployments"][0]["schedule"]["cron"] = "0 4 * * *"
         deploy_config["deployments"][0]["schedule"]["timezone"] = "America/Chicago"
 
@@ -1425,6 +1463,7 @@ async def test_interval_schedule_deployment_yaml(self, prefect_client, work_pool
         with prefect_yaml.open(mode="r") as f:
             deploy_config = yaml.safe_load(f)
 
+        deploy_config["deployments"][0]["name"] = "test-name"
         deploy_config["deployments"][0]["schedule"]["interval"] = 42
         deploy_config["deployments"][0]["schedule"]["anchor_date"] = "2040-02-02"
         deploy_config["deployments"][0]["schedule"]["timezone"] = "America/Chicago"
@@ -1529,7 +1568,7 @@ async def test_providing_multiple_schedules_exits_with_error(self, schedules):
             command="deploy ./flows/hello.py:my_flow -n test-name "
             + " ".join(schedules),
             expected_code=1,
-            expected_output="Only one schedule type can be provided.",
+            expected_output_contains=["Only one schedule type can be provided."],
         )
 
     @pytest.mark.usefixtures("interactive_console", "project_dir")
@@ -1559,6 +1598,9 @@ async def test_deploy_interval_schedule_interactive(
                 # Enter valid interval
                 "42"
                 + readchar.key.ENTER
+                # Decline save
+                + "n"
+                + readchar.key.ENTER
             ),
             expected_code=0,
             expected_output_contains=[
@@ -1602,6 +1644,10 @@ async def test_deploy_cron_schedule_interactive(self, prefect_client, work_pool)
                 +
                 # Select default timezone
                 readchar.key.ENTER
+                +
+                # Decline save
+                "n"
+                + readchar.key.ENTER
             ),
             expected_code=0,
             expected_output_contains=[
@@ -1646,6 +1692,10 @@ async def test_deploy_rrule_schedule_interactive(self, prefect_client, work_pool
                 +
                 # Select default timezone
                 readchar.key.ENTER
+                +
+                # Decline save
+                "n"
+                + readchar.key.ENTER
             ),
             expected_code=0,
         )
@@ -1669,6 +1719,9 @@ async def test_deploy_no_schedule_interactive(self, prefect_client, work_pool):
                 # Decline schedule creation
                 "n"
                 + readchar.key.ENTER
+                # Decline save
+                + "n"
+                + readchar.key.ENTER
             ),
             expected_code=0,
         )
@@ -1952,10 +2005,12 @@ async def test_deploy_with_cli_option_name(
 
         with prefect_file.open(mode="w") as f:
             yaml.safe_dump(contents, f)
-        # Deploy the deployment with an invalid name
         await run_sync_in_worker_thread(
             invoke_and_assert,
-            command="deploy --name from-cli-name",
+            command=(
+                "deploy --name from-cli-name --pool"
+                f" {work_pool.name} ./flows/hello.py:my_flow"
+            ),
             expected_code=0,
             expected_output_contains=[
                 "Deployment 'An important name/from-cli-name' successfully created"
@@ -2165,7 +2220,7 @@ async def test_deploy_with_single_deployment_with_name_in_file(
         # Deploy the deployment with a name
         await run_sync_in_worker_thread(
             invoke_and_assert,
-            command="deploy",
+            command="deploy -n test-name-1",
             expected_code=0,
             expected_output_contains=[
                 "An important name/test-name-1",
@@ -2306,7 +2361,7 @@ async def test_deploy_exits_with_multiple_deployments_with_no_name(
             expected_code=1,
             expected_output_contains=[
                 (
-                    "Discovered multiple deployment configurations, but"
+                    "Discovered one or more deployment configurations, but"
                     " no name was given. Please specify the name of at least one"
                     " deployment to create or update."
                 ),
@@ -2341,15 +2396,14 @@ async def test_deploy_with_single_deployment_with_no_name(
             expected_code=1,
             expected_output_contains=[
                 (
-                    "Could not find deployment configuration with name "
-                    "'test-name-1'. Only CLI options "
-                    "will be used for this deployment."
+                    "Could not find deployment configuration with name 'test-name-1'."
+                    " Your flow will be deployed with a new deployment configuration."
                 ),
             ],
         )
 
     async def test_deploy_exits_with_single_deployment_and_multiple_names(
-        self, project_dir
+        self, project_dir, work_pool
     ):
         prefect_file = Path("prefect.yaml")
         with prefect_file.open(mode="r") as f:
@@ -2359,6 +2413,7 @@ async def test_deploy_exits_with_single_deployment_and_multiple_names(
             {
                 "name": "test-name-1",
                 "entrypoint": "./flows/hello.py:my_flow",
+                "work_pool": {"name": work_pool.name},
             }
         ]
 
@@ -2369,12 +2424,11 @@ async def test_deploy_exits_with_single_deployment_and_multiple_names(
         await run_sync_in_worker_thread(
             invoke_and_assert,
             command="deploy -n test-name-1 -n test-name-2",
-            expected_code=1,
+            expected_code=0,
             expected_output_contains=[
                 (
-                    "Multiple deployment names were provided, but only one deployment"
-                    " configuration was found. Please provide a single deployment"
-                    " name."
+                    "The following deployment(s) could not be found and will not be"
+                    " deployed: test-name-2"
                 ),
             ],
         )
@@ -2391,12 +2445,14 @@ async def test_deploy_select_from_existing_deployments(
             {
                 "name": "test-name-1",
                 "description": "test-description-1",
+                "entrypoint": "./flows/hello.py:my_flow",
                 "work_pool": {"name": work_pool.name},
                 "schedule": {"interval": 3600},
             },
             {
                 "name": "test-name-2",
                 "description": "test-description-2",
+                "entrypoint": "./flows/hello.py:my_flow",
                 "work_pool": {"name": work_pool.name},
                 "schedule": {"interval": 3600},
             },
@@ -2407,9 +2463,9 @@ async def test_deploy_select_from_existing_deployments(
 
         await run_sync_in_worker_thread(
             invoke_and_assert,
-            command="deploy ./flows/hello.py:my_flow",
+            command="deploy",
             expected_code=0,
-            user_input=readchar.key.ENTER,
+            user_input=readchar.key.ENTER + "n" + readchar.key.ENTER,
             expected_output_contains=[
                 "Would you like to use an existing deployment configuration?",
                 "test-name-1",
@@ -2484,3 +2540,266 @@ async def test_migrate_from_deployment_yaml_to_prefect_yaml(self, work_pool):
         assert config["deployments"][1]["entrypoint"] == "flows/hello.py:my_flow"
         assert config["deployments"][1]["schedule"] == {"interval": 3600}
         assert config["deployments"][1]["work_pool"]["name"] == work_pool.name
+
+
+@pytest.mark.usefixtures("interactive_console", "project_dir")
+class TestSaveUserInputs:
+    def test_save_user_inputs_no_existing_prefect_file(self):
+        prefect_file = Path("prefect.yaml")
+        prefect_file.unlink()
+        assert not prefect_file.exists()
+
+        invoke_and_assert(
+            command="deploy flows/hello.py:my_flow",
+            user_input=(
+                # Accept default deployment name
+                readchar.key.ENTER
+                +
+                # decline schedule
+                "n"
+                + readchar.key.ENTER
+                +
+                # accept create work pool
+                readchar.key.ENTER
+                +
+                # choose process work pool
+                readchar.key.ENTER
+                +
+                # enter work pool name
+                "inflatable"
+                + readchar.key.ENTER
+                +
+                # accept save user inputs
+                "y"
+                + readchar.key.ENTER
+            ),
+            expected_code=0,
+            expected_output_contains=[
+                (
+                    "Would you like to save configuration for this deployment for"
+                    " faster deployments in the future?"
+                ),
+                "Deployment configuration saved to prefect.yaml",
+            ],
+        )
+
+        assert prefect_file.exists()
+        with prefect_file.open(mode="r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 1
+        assert config["deployments"][0]["name"] == "default"
+        assert config["deployments"][0]["entrypoint"] == "flows/hello.py:my_flow"
+        assert config["deployments"][0]["schedule"] is None
+        assert config["deployments"][0]["work_pool"]["name"] == "inflatable"
+
+    def test_save_user_inputs_existing_prefect_file(self):
+        prefect_file = Path("prefect.yaml")
+        assert prefect_file.exists()
+
+        invoke_and_assert(
+            command="deploy flows/hello.py:my_flow",
+            user_input=(
+                # Accept default deployment name
+                readchar.key.ENTER
+                +
+                # decline schedule
+                "n"
+                + readchar.key.ENTER
+                +
+                # accept create work pool
+                readchar.key.ENTER
+                +
+                # choose process work pool
+                readchar.key.ENTER
+                +
+                # enter work pool name
+                "inflatable"
+                + readchar.key.ENTER
+                +
+                # accept save user inputs
+                "y"
+                + readchar.key.ENTER
+            ),
+            expected_code=0,
+            expected_output_contains=[
+                (
+                    "Would you like to save configuration for this deployment for"
+                    " faster deployments in the future?"
+                ),
+                "Deployment configuration saved to prefect.yaml",
+            ],
+        )
+
+        with prefect_file.open(mode="r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "default"
+        assert config["deployments"][1]["entrypoint"] == "flows/hello.py:my_flow"
+        assert config["deployments"][1]["schedule"] is None
+        assert config["deployments"][1]["work_pool"]["name"] == "inflatable"
+
+    def test_save_user_inputs_with_interval_schedule(self):
+        invoke_and_assert(
+            command="deploy flows/hello.py:my_flow",
+            user_input=(
+                # Accept default deployment name
+                readchar.key.ENTER
+                +
+                # accept schedule
+                readchar.key.ENTER
+                +
+                # select interval schedule
+                readchar.key.ENTER
+                +
+                # enter interval schedule
+                "3600"
+                + readchar.key.ENTER
+                +
+                # accept create work pool
+                readchar.key.ENTER
+                +
+                # choose process work pool
+                readchar.key.ENTER
+                +
+                # enter work pool name
+                "inflatable"
+                + readchar.key.ENTER
+                +
+                # accept save user inputs
+                "y"
+                + readchar.key.ENTER
+            ),
+            expected_code=0,
+            expected_output_contains=[
+                (
+                    "Would you like to save configuration for this deployment for"
+                    " faster deployments in the future?"
+                ),
+                "Deployment configuration saved to prefect.yaml",
+            ],
+        )
+
+        with open("prefect.yaml", mode="r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "default"
+        assert config["deployments"][1]["entrypoint"] == "flows/hello.py:my_flow"
+        assert config["deployments"][1]["work_pool"]["name"] == "inflatable"
+        assert config["deployments"][1]["schedule"]["interval"] == 3600
+        assert config["deployments"][1]["schedule"]["timezone"] == "UTC"
+        assert config["deployments"][1]["schedule"]["anchor_date"] is not None
+
+    def test_save_user_inputs_with_cron_schedule(self):
+        invoke_and_assert(
+            command="deploy flows/hello.py:my_flow",
+            user_input=(
+                # Accept default deployment name
+                readchar.key.ENTER
+                +
+                # accept schedule
+                readchar.key.ENTER
+                +
+                # select cron schedule
+                readchar.key.DOWN
+                + readchar.key.ENTER
+                +
+                # enter cron schedule
+                "* * * * *"
+                + readchar.key.ENTER
+                +
+                # accept default timezone
+                readchar.key.ENTER
+                +
+                # accept create work pool
+                readchar.key.ENTER
+                +
+                # choose process work pool
+                readchar.key.ENTER
+                +
+                # enter work pool name
+                "inflatable"
+                + readchar.key.ENTER
+                +
+                # accept save user inputs
+                "y"
+                + readchar.key.ENTER
+            ),
+            expected_code=0,
+            expected_output_contains=[
+                (
+                    "Would you like to save configuration for this deployment for"
+                    " faster deployments in the future?"
+                ),
+                "Deployment configuration saved to prefect.yaml",
+            ],
+        )
+
+        with open("prefect.yaml", mode="r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "default"
+        assert config["deployments"][1]["entrypoint"] == "flows/hello.py:my_flow"
+        assert config["deployments"][1]["work_pool"]["name"] == "inflatable"
+        assert config["deployments"][1]["schedule"]["cron"] == "* * * * *"
+        assert config["deployments"][1]["schedule"]["timezone"] == "UTC"
+        assert config["deployments"][1]["schedule"]["day_or"]
+
+    def test_save_user_inputs_with_rrule_schedule(self):
+        invoke_and_assert(
+            command="deploy flows/hello.py:my_flow",
+            user_input=(
+                # Accept default deployment name
+                readchar.key.ENTER
+                +
+                # accept schedule
+                readchar.key.ENTER
+                +
+                # select rrule schedule
+                readchar.key.DOWN
+                + readchar.key.DOWN
+                + readchar.key.ENTER
+                +
+                # enter rrule schedule
+                "FREQ=MINUTELY"
+                + readchar.key.ENTER
+                +
+                # accept default timezone
+                readchar.key.ENTER
+                +
+                # accept create work pool
+                readchar.key.ENTER
+                +
+                # choose process work pool
+                readchar.key.ENTER
+                +
+                # enter work pool name
+                "inflatable"
+                + readchar.key.ENTER
+                +
+                # accept save user inputs
+                "y"
+                + readchar.key.ENTER
+            ),
+            expected_code=0,
+            expected_output_contains=[
+                (
+                    "Would you like to save configuration for this deployment for"
+                    " faster deployments in the future?"
+                ),
+                "Deployment configuration saved to prefect.yaml",
+            ],
+        )
+
+        with open("prefect.yaml", mode="r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "default"
+        assert config["deployments"][1]["entrypoint"] == "flows/hello.py:my_flow"
+        assert config["deployments"][1]["work_pool"]["name"] == "inflatable"
+        assert config["deployments"][1]["schedule"]["rrule"] == "FREQ=MINUTELY"
+        assert config["deployments"][1]["schedule"]["timezone"] == "UTC"