Repository: PrefectHQ/prefect

Issue:
Allow `prefect deploy` to be run prior to project initialization
The `prefect deploy` command will fail if there is no `prefect.yaml` file in the current working directory. We can remove this requirement to lower friction during deployment creation. To remove this requirement, we'll need to inspect the current working directory to create some sensible deployment lifecycle actions. 

The CLI can follow these steps to attempt to create a deployment without a `prefect.yaml`:
1. If the current working directory is a git repository with a remote origin configured, ask if they would like to pull their flow code from this repository when running their deployment. 
    - If it is a private repository, also ask for an access token to clone it. 
    - Configure a `git_clone_project` pull step based on the user's inputs
2. If the current working directory is not a git repository, configure a `set_working_directory` pull step.
3. If only a `set_working_directory` step was configured, print a message informing the user that their newly created deployment will work with a locally running process worker but that they'll need to configure a project to run their deployment remotely. Print the `prefect project init` command as guidance.


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
@@ -3,6 +3,7 @@
 """
 from datetime import timedelta
 from rich.prompt import PromptBase, InvalidResponse
+from rich.text import Text
 
 from prefect.client.schemas.schedules import (
     SCHEDULE_TYPES,
@@ -11,14 +12,23 @@
     RRuleSchedule,
 )
 
-from typing import Dict, List, Optional
+from typing import Any, Dict, List, Optional
 import readchar
 
+from rich.console import Console, Group
 from rich.table import Table
 from rich.live import Live
 from rich.prompt import Prompt, Confirm
 from prefect.cli._utilities import exit_with_error
 
+from prefect.client.utilities import inject_client
+
+from prefect.client.orchestration import PrefectClient
+
+from prefect.client.collections import get_collections_metadata_client
+
+from prefect.client.schemas.actions import WorkPoolCreate
+
 
 def prompt(message, **kwargs):
     """Utility to prompt the user for input with consistent styling"""
@@ -36,6 +46,8 @@ def prompt_select_from_table(
     columns: List[Dict],
     data: List[Dict],
     table_kwargs: Optional[Dict] = None,
+    opt_out_message: Optional[str] = None,
+    opt_out_response: Any = None,
 ) -> Dict:
     """
     Given a list of columns and some data, display options to user in a table
@@ -55,7 +67,6 @@ def prompt_select_from_table(
     """
     current_idx = 0
     selected_row = None
-    first_run = True
     table_kwargs = table_kwargs or {}
 
     def build_table() -> Table:
@@ -63,12 +74,6 @@ def build_table() -> Table:
         Generate a table of options. The `current_idx` will be highlighted.
         """
 
-        nonlocal first_run
-        if first_run:
-            console.print(
-                f"[bold][green]?[/] {prompt} [bright_blue][Use arrows to move; enter to"
-                " select][/]"
-            )
         table = Table(**table_kwargs)
         table.add_column()
         for column in columns:
@@ -84,10 +89,21 @@ def build_table() -> Table:
                 table.add_row("[bold][blue]>", f"[bold][blue]{row[0]}[/]", *row[1:])
             else:
                 table.add_row("  ", *row)
-        first_run = False
+
+        if opt_out_message:
+            prefix = "  > " if current_idx == len(data) else " " * 4
+            bottom_text = Text(prefix + opt_out_message)
+            if current_idx == len(data):
+                bottom_text.stylize("bold blue")
+            return Group(table, bottom_text)
+
         return table
 
     with Live(build_table(), auto_refresh=False, console=console) as live:
+        live.console.print(
+            f"[bold][green]?[/] {prompt} [bright_blue][Use arrows to move; enter to"
+            " select][/]"
+        )
         while selected_row is None:
             key = readchar.readkey()
 
@@ -99,13 +115,18 @@ def build_table() -> Table:
             elif key == readchar.key.DOWN:
                 current_idx = current_idx + 1
                 # wrap to top if at the bottom
-                if current_idx >= len(data):
+                if opt_out_message and current_idx >= len(data) + 1:
+                    current_idx = 0
+                elif not opt_out_message and current_idx >= len(data):
                     current_idx = 0
             elif key == readchar.key.CTRL_C:
                 # gracefully exit with no message
                 exit_with_error("")
             elif key == readchar.key.ENTER or key == readchar.key.CR:
-                selected_row = data[current_idx]
+                if current_idx >= len(data):
+                    selected_row = opt_out_response
+                else:
+                    selected_row = data[current_idx]
 
             live.update(build_table(), refresh=True)
 
@@ -282,3 +303,74 @@ def prompt_schedule(console) -> SCHEDULE_TYPES:
         return prompt_rrule_schedule(console)
     else:
         raise Exception("Invalid schedule type")
+
+
+@inject_client
+async def prompt_select_work_pool(
+    console: Console,
+    prompt: str = "Which work pool would you like to deploy this flow to?",
+    client: PrefectClient = None,
+) -> str:
+    work_pools = await client.read_work_pools()
+    work_pool_options = [
+        work_pool.dict()
+        for work_pool in work_pools
+        if work_pool.type != "prefect-agent"
+    ]
+    if not work_pool_options:
+        work_pool = await prompt_create_work_pool(console, client=client)
+        return work_pool.name
+    else:
+        selected_work_pool_row = prompt_select_from_table(
+            console,
+            prompt,
+            [
+                {"header": "Work Pool Name", "key": "name"},
+                {"header": "Infrastructure Type", "key": "type"},
+                {"header": "Description", "key": "description"},
+            ],
+            work_pool_options,
+        )
+        return selected_work_pool_row["name"]
+
+
+@inject_client
+async def prompt_create_work_pool(
+    console: Console,
+    client: PrefectClient = None,
+):
+    if not confirm(
+        (
+            "Looks like you don't have any work pools this flow can be deployed to."
+            " Would you like to create one?"
+        ),
+        default=True,
+        console=console,
+    ):
+        raise ValueError(
+            "A work pool is required to deploy this flow. Please specify a work pool"
+            " name via the '--pool' flag or in your deployment.yaml file."
+        )
+    async with get_collections_metadata_client() as collections_client:
+        worker_metadata = await collections_client.read_worker_metadata()
+    selected_worker_row = prompt_select_from_table(
+        console,
+        prompt="What infrastructure type would you like to use for your new work pool?",
+        columns=[
+            {"header": "Type", "key": "type"},
+            {"header": "Description", "key": "description"},
+        ],
+        data=[
+            worker
+            for collection in worker_metadata.values()
+            for worker in collection.values()
+            if worker["type"] != "prefect-agent"
+        ],
+        table_kwargs={"show_lines": True},
+    )
+    work_pool_name = prompt("Work pool name")
+    work_pool = await client.create_work_pool(
+        WorkPoolCreate(name=work_pool_name, type=selected_worker_row["type"])
+    )
+    console.print(f"Your work pool {work_pool.name!r} has been created!", style="green")
+    return work_pool
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
@@ -1,4 +1,5 @@
 """Module containing implementation for deploying projects."""
+from getpass import GetPassWarning
 import json
 from copy import deepcopy
 from datetime import timedelta
@@ -8,12 +9,9 @@
 import typer
 import typer.core
 import yaml
-from rich.console import Console
 from rich.panel import Panel
+from rich.console import Console
 
-import prefect
-import prefect.context
-import prefect.settings
 from prefect.cli._utilities import (
     exit_with_error,
 )
@@ -22,11 +20,9 @@
     confirm,
     prompt_select_from_table,
     prompt_schedule,
+    prompt_select_work_pool,
 )
 from prefect.cli.root import app, is_interactive
-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.schedules import (
     CronSchedule,
     IntervalSchedule,
@@ -36,13 +32,21 @@
 from prefect.exceptions import ObjectNotFound
 from prefect.flows import load_flow_from_entrypoint
 from prefect.projects import find_prefect_directory, register_flow
-from prefect.settings import PREFECT_UI_URL
+from prefect.settings import PREFECT_UI_URL, PREFECT_DEBUG_MODE
 from prefect.utilities.asyncutils import run_sync_in_worker_thread
 from prefect.utilities.callables import parameter_schema
 from prefect.utilities.templating import apply_values
 
 from prefect.projects.steps.core import run_steps
 
+from prefect.projects.base import _get_git_branch, _get_git_remote_origin_url
+
+from prefect.blocks.system import Secret
+
+from prefect.utilities.slugify import slugify
+
+from prefect.client.orchestration import PrefectClient
+
 
 @app.command()
 async def deploy(
@@ -195,16 +199,7 @@ async def deploy(
         with open("prefect.yaml", "r") as f:
             project = yaml.safe_load(f)
     except FileNotFoundError:
-        app.console.print(
-            "We were unable to find a prefect.yaml file in the current directory.",
-            style="red",
-        )
-        app.console.print(
-            "\nTo get started deploying flows please initialize a new project:",
-            style="red",
-        )
-        app.console.print("\n\t$ prefect project init\n", style="blue")
-        raise typer.Exit(1)
+        project = {}
 
     try:
         with open("deployment.yaml", "r") as f:
@@ -216,10 +211,14 @@ async def deploy(
             else:
                 deployments = [base_deploy]
     except FileNotFoundError:
-        app.console.print(
-            "No deployment.yaml file found, only provided CLI options will be used.",
-            style="yellow",
-        )
+        if PREFECT_DEBUG_MODE:
+            app.console.print(
+                (
+                    "No deployment.yaml file found, only provided CLI options will be"
+                    " used."
+                ),
+                style="yellow",
+            )
         deployments = []
 
     try:
@@ -267,9 +266,12 @@ async def deploy(
                     )
                 selected_deployment = prompt_select_from_table(
                     app.console,
-                    "Which deployment would you like to create or update?",
+                    (
+                        "Would you like to use an existing deployment configuration"
+                        " from deployment.yaml?"
+                    ),
                     [
-                        {"header": "Deployment Name", "key": "name"},
+                        {"header": "Name", "key": "name"},
                         {"header": "Description", "key": "description"},
                     ],
                     [
@@ -277,6 +279,8 @@ async def deploy(
                         for deployment in deployments
                         if deployment.get("name")
                     ],
+                    opt_out_message="No, configure a new deployment",
+                    opt_out_response={},
                 )
                 await _run_single_deploy(
                     base_deploy=selected_deployment,
@@ -303,8 +307,13 @@ async def deploy(
         exit_with_error(str(exc))
 
 
+@inject_client
 async def _run_single_deploy(
-    base_deploy: Dict, project: Dict, options: Optional[Dict] = None, ci: bool = False
+    base_deploy: Dict,
+    project: Dict,
+    options: Optional[Dict] = None,
+    ci: bool = False,
+    client: PrefectClient = None,
 ):
     base_deploy = deepcopy(base_deploy) if base_deploy else {}
     project = deepcopy(project) if project else {}
@@ -335,9 +344,8 @@ async def _run_single_deploy(
     )
     timezone = options.get("timezone") or base_deploy_schedule.get("timezone")
 
-    build_steps = base_deploy.get("build", project.get("build", [])) or []
-    pull_steps = base_deploy.get("pull", project.get("pull", [])) or []
-    push_steps = base_deploy.get("push", project.get("push", [])) or []
+    build_steps = base_deploy.get("build", project.get("build")) or []
+    push_steps = base_deploy.get("push", project.get("push")) or []
 
     if interval_anchor and not interval:
         raise ValueError(
@@ -374,6 +382,12 @@ async def _run_single_deploy(
                 " entrypoint is in the format path/to/file.py:flow_fn_name and the"
                 " file name and flow function name are correct."
             )
+        except FileNotFoundError:
+            if PREFECT_DEBUG_MODE:
+                app.console.print(
+                    "Could not find .prefect directory. Flow entrypoint will not be"
+                    " registered."
+                )
         flow_name = flow.name
     elif flow_name:
         prefect_dir = find_prefect_directory()
@@ -450,24 +464,70 @@ async def _run_single_deploy(
         ci=ci,
     )
 
+    variable_overrides = {}
+    for variable in variables or []:
+        key, value = variable.split("=", 1)
+        variable_overrides[key] = value
+
+    if work_pool_name:
+        base_deploy["work_pool"]["name"] = work_pool_name
+    if work_queue_name:
+        base_deploy["work_pool"]["work_queue_name"] = work_queue_name
+
+    base_deploy["work_pool"]["job_variables"].update(variable_overrides)
+
+    # determine work pool
+    if base_deploy["work_pool"]["name"]:
+        try:
+            work_pool = await client.read_work_pool(base_deploy["work_pool"]["name"])
+
+            # dont allow submitting to prefect-agent typed work pools
+            if work_pool.type == "prefect-agent":
+                if not is_interactive() or ci:
+                    raise ValueError(
+                        "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."
+                    )
+                app.console.print(
+                    "You've chosen a work pool with type 'prefect-agent' which"
+                    " cannot be used for project-style deployments. Let's pick"
+                    " another work pool to deploy to."
+                )
+                base_deploy["work_pool"]["name"] = await prompt_select_work_pool(
+                    app.console,
+                    client=client,
+                )
+        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."
+            )
+    else:
+        if not is_interactive() or ci:
+            raise ValueError(
+                "A work pool is required to deploy this flow. Please specify a work"
+                " pool name via the '--pool' flag or in your deployment.yaml file."
+            )
+        base_deploy["work_pool"]["name"] = await prompt_select_work_pool(
+            console=app.console, client=client
+        )
+
     ## RUN BUILD AND PUSH STEPS
     step_outputs = {}
     if build_steps:
         app.console.print("Running deployment build steps...")
-    step_outputs.update(
-        await run_steps(build_steps, step_outputs, print_function=app.console.print)
-    )
+        step_outputs.update(
+            await run_steps(build_steps, step_outputs, print_function=app.console.print)
+        )
 
     if push_steps:
         app.console.print("Running deployment push steps...")
-    step_outputs.update(
-        await run_steps(push_steps, step_outputs, print_function=app.console.print)
-    )
-
-    variable_overrides = {}
-    for variable in variables or []:
-        key, value = variable.split("=", 1)
-        variable_overrides[key] = value
+        step_outputs.update(
+            await run_steps(push_steps, step_outputs, print_function=app.console.print)
+        )
 
     step_outputs.update(variable_overrides)
 
@@ -484,13 +544,6 @@ async def _run_single_deploy(
     elif not base_deploy["description"]:
         base_deploy["description"] = flow.description
 
-    if work_pool_name:
-        base_deploy["work_pool"]["name"] = work_pool_name
-    if work_queue_name:
-        base_deploy["work_pool"]["work_queue_name"] = work_queue_name
-
-    base_deploy["work_pool"]["job_variables"].update(variable_overrides)
-
     ## apply templating from build and push steps to the final deployment spec
     _parameter_schema = base_deploy.pop("parameter_openapi_schema")
     base_deploy = apply_values(base_deploy, step_outputs)
@@ -500,118 +553,67 @@ async def _run_single_deploy(
     base_deploy["schedule"] = schedule
 
     # prepare the pull step
+    pull_steps = base_deploy.get(
+        "pull", project.get("pull")
+    ) or await _generate_default_pull_action(
+        app.console,
+        base_deploy=base_deploy,
+        ci=ci,
+    )
     pull_steps = apply_values(pull_steps, step_outputs)
 
-    async with prefect.get_client() as client:
-        flow_id = await client.create_flow_from_name(base_deploy["flow_name"])
-
-        if base_deploy["work_pool"]["name"]:
-            try:
-                work_pool = await client.read_work_pool(
-                    base_deploy["work_pool"]["name"]
-                )
-
-                # dont allow submitting to prefect-agent typed work pools
-                if work_pool.type == "prefect-agent":
-                    if not is_interactive() or ci:
-                        raise ValueError(
-                            "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."
-                        )
-                    app.console.print(
-                        "You've chosen a work pool with type 'prefect-agent' which"
-                        " cannot be used for project-style deployments. Let's pick"
-                        " another work pool to deploy to."
-                    )
-                    base_deploy["work_pool"]["name"] = await _prompt_select_work_pool(
-                        app.console,
-                        client=client,
-                    )
-            except ObjectNotFound:
-                app.console.print(
-                    (
-                        "\nThis 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."
-                    ),
-                    style="red",
-                )
-        else:
-            if not is_interactive() or ci:
-                raise ValueError(
-                    "A work pool is required to deploy this flow. Please specify a work"
-                    " pool name via the '--pool' flag or in your deployment.yaml file."
-                )
-            base_deploy["work_pool"]["name"] = await _prompt_select_work_pool(
-                console=app.console, client=client
-            )
+    flow_id = await client.create_flow_from_name(base_deploy["flow_name"])
+
+    deployment_id = await client.create_deployment(
+        flow_id=flow_id,
+        name=base_deploy["name"],
+        work_queue_name=base_deploy["work_pool"]["work_queue_name"],
+        work_pool_name=base_deploy["work_pool"]["name"],
+        version=base_deploy["version"],
+        schedule=base_deploy["schedule"],
+        parameters=base_deploy["parameters"],
+        description=base_deploy["description"],
+        tags=base_deploy["tags"],
+        path=base_deploy.get("path"),
+        entrypoint=base_deploy["entrypoint"],
+        parameter_openapi_schema=base_deploy["parameter_openapi_schema"].dict(),
+        pull_steps=pull_steps,
+        infra_overrides=base_deploy["work_pool"]["job_variables"],
+    )
 
-        deployment_id = await client.create_deployment(
-            flow_id=flow_id,
-            name=base_deploy["name"],
-            work_queue_name=base_deploy["work_pool"]["work_queue_name"],
-            work_pool_name=base_deploy["work_pool"]["name"],
-            version=base_deploy["version"],
-            schedule=base_deploy["schedule"],
-            parameters=base_deploy["parameters"],
-            description=base_deploy["description"],
-            tags=base_deploy["tags"],
-            path=base_deploy.get("path"),
-            entrypoint=base_deploy["entrypoint"],
-            parameter_openapi_schema=base_deploy["parameter_openapi_schema"].dict(),
-            pull_steps=pull_steps,
-            infra_overrides=base_deploy["work_pool"]["job_variables"],
-        )
+    app.console.print(
+        Panel(
+            f"Deployment '{base_deploy['flow_name']}/{base_deploy['name']}'"
+            f" successfully created with id '{deployment_id}'."
+        ),
+        style="green",
+    )
 
+    if PREFECT_UI_URL:
         app.console.print(
-            (
-                f"Deployment '{base_deploy['flow_name']}/{base_deploy['name']}'"
-                f" successfully created with id '{deployment_id}'."
-            ),
-            style="green",
+            "\nView Deployment in UI:"
+            f" {PREFECT_UI_URL.value()}/deployments/deployment/{deployment_id}"
         )
 
-        if PREFECT_UI_URL:
-            app.console.print(
-                "View Deployment in UI:"
-                f" {PREFECT_UI_URL.value()}/deployments/deployment/{deployment_id}"
-            )
-
-        if base_deploy["work_pool"]["name"] is not None:
-            app.console.print(
-                "\nTo execute flow runs from this deployment, start a worker in a"
-                " separate terminal that pulls work from the"
-                f" {base_deploy['work_pool']['name']!r} work pool:"
-            )
-            app.console.print(
-                (
-                    "\n\t$ prefect worker start --pool"
-                    f" {base_deploy['work_pool']['name']!r}"
-                ),
-                style="blue",
-            )
-            app.console.print(
-                "\nTo schedule a run for this deployment, use the following command:"
-            )
-            app.console.print(
-                (
-                    "\n\t$ prefect deployment run"
-                    f" '{base_deploy['flow_name']}/{base_deploy['name']}'\n"
-                ),
-                style="blue",
-            )
-        else:
-            app.console.print(
-                (
-                    "\nThis deployment does not specify a work pool or queue, which"
-                    " means no worker will be able to pick up its runs. To add a"
-                    " work pool, edit the deployment spec and re-run this command,"
-                    " or visit the deployment in the UI."
-                ),
-                style="red",
-            )
+    app.console.print(
+        "\nTo execute flow runs from this deployment, start a worker in a"
+        " separate terminal that pulls work from the"
+        f" {base_deploy['work_pool']['name']!r} work pool:"
+    )
+    app.console.print(
+        f"\n\t$ prefect worker start --pool {base_deploy['work_pool']['name']!r}",
+        style="blue",
+    )
+    app.console.print(
+        "\nTo schedule a run for this deployment, use the following command:"
+    )
+    app.console.print(
+        (
+            "\n\t$ prefect deployment run"
+            f" '{base_deploy['flow_name']}/{base_deploy['name']}'\n"
+        ),
+        style="blue",
+    )
 
 
 async def _run_multi_deploy(
@@ -664,77 +666,6 @@ async def _run_multi_deploy(
             await _run_single_deploy(base_deploy, project)
 
 
-@inject_client
-async def _prompt_select_work_pool(
-    console: Console,
-    prompt: str = "Which work pool would you like to deploy this flow to?",
-    client: PrefectClient = None,
-) -> str:
-    work_pools = await client.read_work_pools()
-    work_pool_options = [
-        work_pool.dict()
-        for work_pool in work_pools
-        if work_pool.type != "prefect-agent"
-    ]
-    if not work_pool_options:
-        work_pool = await _prompt_create_work_pool(console, client=client)
-        return work_pool.name
-    else:
-        selected_work_pool_row = prompt_select_from_table(
-            console,
-            prompt,
-            [
-                {"header": "Work Pool Name", "key": "name"},
-                {"header": "Infrastructure Type", "key": "type"},
-                {"header": "Description", "key": "description"},
-            ],
-            work_pool_options,
-        )
-        return selected_work_pool_row["name"]
-
-
-@inject_client
-async def _prompt_create_work_pool(
-    console: Console,
-    client: PrefectClient = None,
-):
-    if not confirm(
-        (
-            "Looks like you don't have any work pools this flow can be deployed to."
-            " Would you like to create one?"
-        ),
-        default=True,
-        console=app.console,
-    ):
-        raise ValueError(
-            "A work pool is required to deploy this flow. Please specify a work pool"
-            " name via the '--pool' flag or in your deployment.yaml file."
-        )
-    async with get_collections_metadata_client() as collections_client:
-        worker_metadata = await collections_client.read_worker_metadata()
-    selected_worker_row = prompt_select_from_table(
-        console,
-        prompt="What infrastructure type would you like to use for your new work pool?",
-        columns=[
-            {"header": "Type", "key": "type"},
-            {"header": "Description", "key": "description"},
-        ],
-        data=[
-            worker
-            for collection in worker_metadata.values()
-            for worker in collection.values()
-            if worker["type"] != "prefect-agent"
-        ],
-        table_kwargs={"show_lines": True},
-    )
-    work_pool_name = prompt("Work pool name")
-    work_pool = await client.create_work_pool(
-        WorkPoolCreate(name=work_pool_name, type=selected_worker_row["type"])
-    )
-    console.print(f"Your work pool {work_pool.name!r} has been created!", style="green")
-    return work_pool
-
-
 def _construct_schedule(
     cron: Optional[str] = None,
     timezone: Optional[str] = None,
@@ -822,3 +753,117 @@ def _merge_with_default_deployment(base_deploy: Dict):
                     base_deploy[key][k] = v
 
     return base_deploy
+
+
+async def _generate_default_pull_action(
+    console: Console, base_deploy: Dict, ci: bool = False
+):
+    remote_url = _get_git_remote_origin_url()
+    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,
+        )
+    ):
+        branch = _get_git_branch() or "main"
+
+        if not confirm(
+            f"Is [green]{remote_url}[/] the correct URL to pull your flow code from?",
+            default=True,
+            console=console,
+        ):
+            remote_url = prompt(
+                "Please enter the URL to pull your flow code from", console=console
+            )
+        if not confirm(
+            f"Is [green]{branch}[/] the correct branch to pull your flow code from?",
+            default=True,
+            console=console,
+        ):
+            branch = prompt(
+                "Please enter the branch to pull your flow code from",
+                default="main",
+                console=console,
+            )
+        token_secret_block_name = None
+        if confirm("Is this a private repository?", console=console):
+            token_secret_block_name = f"deployment-{slugify(base_deploy['name'])}-{slugify(base_deploy['flow_name'])}-repo-token"
+            create_new_block = False
+            prompt_message = (
+                "Please enter a token that can be used to access your private"
+                " repository. This token will be saved as a secret via the Prefect API"
+            )
+
+            try:
+                await Secret.load(token_secret_block_name)
+                if not confirm(
+                    (
+                        "We found an existing token saved for this deployment. Would"
+                        " you like use the existing token?"
+                    ),
+                    default=True,
+                    console=console,
+                ):
+                    prompt_message = (
+                        "Please enter a token that can be used to access your private"
+                        " repository (this will overwrite the existing token saved via"
+                        " the Prefect API)."
+                    )
+
+                    create_new_block = True
+            except ValueError:
+                create_new_block = True
+
+            if create_new_block:
+                try:
+                    repo_token = prompt(
+                        prompt_message,
+                        console=console,
+                        password=True,
+                    )
+                except GetPassWarning:
+                    # Handling for when password masking is not supported
+                    repo_token = prompt(
+                        prompt_message,
+                        console=console,
+                    )
+                await Secret(
+                    value=repo_token,
+                ).save(name=token_secret_block_name, overwrite=True)
+
+        git_clone_step = {
+            "prefect.projects.steps.git_clone_project": {
+                "repository": remote_url,
+                "branch": branch,
+            }
+        }
+
+        if token_secret_block_name:
+            git_clone_step["prefect.projects.steps.git_clone_project"]["token"] = (
+                "{{ prefect.blocks.secret." + token_secret_block_name + " }}"
+            )
+
+        return [git_clone_step]
+    else:
+        entrypoint_path, _ = base_deploy["entrypoint"].split(":")
+        console.print(
+            "Your Prefect workers will attempt to load your flow from:"
+            f" [green]{(Path.cwd()/Path(entrypoint_path)).absolute().resolve()}[/]. To"
+            " see more options for managing your flow's code, run:\n\n\t[blue]$"
+            " prefect project recipes ls[/]\n"
+        )
+        return [
+            {
+                "prefect.projects.steps.set_working_directory": {
+                    "directory": str(Path.cwd().absolute().resolve())
+                }
+            }
+        ]
diff --git a/src/prefect/projects/base.py b/src/prefect/projects/base.py
--- a/src/prefect/projects/base.py
+++ b/src/prefect/projects/base.py
@@ -174,6 +174,36 @@ def configure_project_by_recipe(recipe: str, **formatting_kwargs) -> dict:
     return config
 
 
+def _get_git_remote_origin_url() -> Optional[str]:
+    """
+    Returns the git remote origin URL for the current directory.
+    """
+    try:
+        origin_url = subprocess.check_output(
+            ["git", "config", "--get", "remote.origin.url"],
+            shell=sys.platform == "win32",
+            stderr=subprocess.DEVNULL,
+        )
+        origin_url = origin_url.decode().strip()
+    except subprocess.CalledProcessError:
+        return None
+
+    return origin_url
+
+
+def _get_git_branch() -> Optional[str]:
+    """
+    Returns the git branch for the current directory.
+    """
+    try:
+        branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"])
+        branch = branch.decode().strip()
+    except subprocess.CalledProcessError:
+        return None
+
+    return branch
+
+
 def initialize_project(
     name: str = None, recipe: str = None, inputs: dict = None
 ) -> List[str]:
@@ -194,26 +224,12 @@ def initialize_project(
     formatting_kwargs = {"directory": str(Path(".").absolute().resolve())}
     dir_name = os.path.basename(os.getcwd())
 
-    try:
-        p = subprocess.check_output(
-            ["git", "remote", "get-url", "origin"],
-            shell=sys.platform == "win32",
-            stderr=subprocess.DEVNULL,
-        )
-        formatting_kwargs["repository"] = p.decode().strip()
+    remote_url = _get_git_remote_origin_url()
+    if remote_url:
+        formatting_kwargs["repository"] = remote_url
         is_git_based = True
-        try:
-            p = subprocess.check_output(
-                ["git", "rev-parse", "--abbrev-ref", "HEAD"],
-                shell=sys.platform == "win32",
-                stderr=subprocess.DEVNULL,
-            )
-            formatting_kwargs["branch"] = p.decode().strip()
-        except subprocess.CalledProcessError:
-            formatting_kwargs["branch"] = "main"
-
-    except subprocess.CalledProcessError:
-        pass
+        branch = _get_git_branch()
+        formatting_kwargs["branch"] = branch or "main"
 
     formatting_kwargs["name"] = dir_name
 


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
@@ -1,5 +1,6 @@
 import os
 import shutil
+import subprocess
 import sys
 from datetime import timedelta
 from pathlib import Path
@@ -509,23 +510,288 @@ async def test_project_deploy_with_no_deployment_file(
         assert deployment.tags == ["foo-bar"]
         assert deployment.infra_overrides == {"env": "prod"}
 
-    async def test_project_deploy_with_no_prefect_yaml(self, project_dir):
+    async def test_project_deploy_with_no_prefect_yaml(self, project_dir, work_pool):
         Path(project_dir, "prefect.yaml").unlink()
 
         await run_sync_in_worker_thread(
             invoke_and_assert,
             command=(
-                "deploy ./flows/hello.py:my_flow -n test-name -p test-pool --version"
-                " 1.0.0 -v env=prod -t foo-bar"
+                "deploy ./flows/hello.py:my_flow -n test-name -p"
+                f" {work_pool.name} --version 1.0.0 -v env=prod -t foo-bar"
             ),
-            expected_code=1,
+            expected_code=0,
             expected_output_contains=[
-                "We were unable to find a prefect.yaml file in the current directory.",
-                "To get started deploying flows please initialize a new project:",
-                "prefect project init",
+                "Your Prefect workers will attempt to load your flow from:",
+                "To see more options for managing your flow's code, run:",
+                "$ prefect project recipes ls",
             ],
         )
 
+    class TestGeneratedPullAction:
+        @pytest.fixture
+        def uninitialized_project_dir(self, project_dir):
+            Path(project_dir, "prefect.yaml").unlink()
+            Path(project_dir, "deployment.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_generates_pull_action(
+            self, work_pool, prefect_client, uninitialized_project_dir
+        ):
+            await run_sync_in_worker_thread(
+                invoke_and_assert,
+                command=(
+                    "deploy flows/hello.py:my_flow -n test-name -p"
+                    f" {work_pool.name} --interval 60"
+                ),
+                expected_code=0,
+            )
+
+            deployment = await prefect_client.read_deployment_by_name(
+                "An important name/test-name"
+            )
+            assert deployment.pull_steps == [
+                {
+                    "prefect.projects.steps.set_working_directory": {
+                        "directory": str(uninitialized_project_dir)
+                    }
+                }
+            ]
+
+        @pytest.mark.usefixtures("interactive_console")
+        async def test_project_deploy_with_no_prefect_yaml_git_repo_no_remote(
+            self,
+            work_pool,
+            prefect_client,
+            uninitialized_project_dir_with_git_no_remote,
+        ):
+            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,
+            )
+
+            deployment = await prefect_client.read_deployment_by_name(
+                "An important name/test-name"
+            )
+            assert deployment.pull_steps == [
+                {
+                    "prefect.projects.steps.set_working_directory": {
+                        "directory": str(uninitialized_project_dir_with_git_no_remote)
+                    }
+                }
+            ]
+
+        @pytest.mark.usefixtures("interactive_console")
+        async def test_project_deploy_with_no_prefect_yaml_git_repo_user_rejects(
+            self,
+            work_pool,
+            prefect_client,
+            uninitialized_project_dir_with_git_with_remote,
+        ):
+            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"
+                ),
+                # User rejects pulling from the remote repo
+                user_input="n" + readchar.key.ENTER,
+                expected_code=0,
+            )
+
+            deployment = await prefect_client.read_deployment_by_name(
+                "An important name/test-name"
+            )
+            assert deployment.pull_steps == [
+                {
+                    "prefect.projects.steps.set_working_directory": {
+                        "directory": str(uninitialized_project_dir_with_git_with_remote)
+                    }
+                }
+            ]
+
+        @pytest.mark.usefixtures(
+            "interactive_console", "uninitialized_project_dir_with_git_with_remote"
+        )
+        async def test_project_deploy_with_no_prefect_yaml_git_repo(
+            self, work_pool, prefect_client
+        ):
+            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 git origin
+                    readchar.key.ENTER
+                    +
+                    # Accept discovered URL
+                    readchar.key.ENTER
+                    +
+                    # Accept discovered branch
+                    readchar.key.ENTER
+                    +
+                    # Choose public repo
+                    "n"
+                    + readchar.key.ENTER
+                ),
+                expected_output_contains=[
+                    "Would you like your workers to pull your flow code from its remote"
+                    " repository when running this flow?"
+                ],
+            )
+
+            deployment = await prefect_client.read_deployment_by_name(
+                "An important name/test-name"
+            )
+            assert deployment.pull_steps == [
+                {
+                    "prefect.projects.steps.git_clone_project": {
+                        "repository": "https://example.com/org/repo.git",
+                        "branch": "main",
+                    }
+                }
+            ]
+
+        @pytest.mark.usefixtures(
+            "interactive_console", "uninitialized_project_dir_with_git_with_remote"
+        )
+        async def test_project_deploy_with_no_prefect_yaml_git_repo_user_overrides(
+            self, work_pool, prefect_client
+        ):
+            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 git origin
+                    readchar.key.ENTER
+                    +
+                    # Reject discovered URL
+                    "n"
+                    + readchar.key.ENTER
+                    +
+                    # Enter new URL
+                    "https://example.com/org/repo-override.git"
+                    + readchar.key.ENTER
+                    +
+                    # Reject discovered branch
+                    "n"
+                    + readchar.key.ENTER
+                    +
+                    # Enter new branch
+                    "dev"
+                    + readchar.key.ENTER
+                    +
+                    # Choose public repo
+                    "n"
+                    + readchar.key.ENTER
+                ),
+                expected_output_contains=[
+                    "Would you like your workers to pull your flow code from its remote"
+                    " repository when running this flow?"
+                ],
+            )
+
+            deployment = await prefect_client.read_deployment_by_name(
+                "An important name/test-name"
+            )
+            assert deployment.pull_steps == [
+                {
+                    "prefect.projects.steps.git_clone_project": {
+                        "repository": "https://example.com/org/repo-override.git",
+                        "branch": "dev",
+                    }
+                }
+            ]
+
+        @pytest.mark.usefixtures(
+            "interactive_console", "uninitialized_project_dir_with_git_with_remote"
+        )
+        async def test_project_deploy_with_no_prefect_yaml_git_repo_with_token(
+            self, work_pool, prefect_client
+        ):
+            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 git origin
+                    readchar.key.ENTER
+                    +
+                    # Accept discovered URL
+                    readchar.key.ENTER
+                    +
+                    # Accept discovered branch
+                    readchar.key.ENTER
+                    +
+                    # Choose private repo
+                    "y"
+                    + readchar.key.ENTER
+                    # Enter token
+                    + "my-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?"
+                ],
+            )
+
+            deployment = await prefect_client.read_deployment_by_name(
+                "An important name/test-name"
+            )
+            assert deployment.pull_steps == [
+                {
+                    "prefect.projects.steps.git_clone_project": {
+                        "repository": "https://example.com/org/repo.git",
+                        "branch": "main",
+                        "token": (
+                            "{{ prefect.blocks.secret.deployment-test-name-an-important-name-repo-token }}"
+                        ),
+                    }
+                }
+            ]
+
+            token_block = await Secret.load(
+                "deployment-test-name-an-important-name-repo-token"
+            )
+            assert token_block.get() == "my-token"
+
     async def test_project_deploy_with_empty_dep_file(
         self, project_dir, prefect_client
     ):
@@ -2046,7 +2312,10 @@ async def test_deploy_select_from_existing_deployments(
             expected_code=0,
             user_input=readchar.key.ENTER,
             expected_output_contains=[
-                "Which deployment would you like to create or update?",
+                (
+                    "Would you like to use an existing deployment configuration from"
+                    " deployment.yaml?"
+                ),
                 "test-name-1",
                 "test-name-2",
                 "test-description-1",