Safeguard push-mode auto-push (#639)

Push-mode updates were too eager: `edit`, `apply`, and `update`
configured the deployment remote before pushing, so any current git repo
could become the source repo by accident.

This keeps create smooth, but makes updates require an existing
`llamaagents-<deployment>` remote before auto-pushing. `--push` is the
explicit escape hatch for linking and pushing the current repo, and
`--no-push` still skips the push entirely.

Docs now describe the safer default for push-mode deployments.
This commit is contained in:
Adrian Lyjak
2026-05-14 17:57:50 -04:00
committed by GitHub
parent 06ff626efe
commit abc176e6ce
9 changed files with 408 additions and 29 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"llamactl": patch
---
Only auto-push push-mode updates from repos with the deployment remote configured.
@@ -32,7 +32,18 @@ Notes:
- `-o json` and `-o yaml` are for scripts. Status messages go to stderr; structured data goes to stdout.
- `-f -` reads YAML from stdin on commands that accept `-f`.
- `repo_url: ""` in apply YAML means push the current local working tree. Push-capable deployment mutations support `--no-push` when you have already pushed separately or want the server to use the revision it can already resolve.
- `repo_url: ""` in apply YAML means push-mode. The deployment stores code in LlamaCloud's internal git repo instead of pulling from an external Git URL.
### Push-mode auto-push
Push-mode commands can mirror local code to the deployment's internal git repo. The local git remote is named `llamaagents-NAME`.
- `create` configures that remote and pushes by default.
- `edit`, `apply`, and `update` auto-push only when the current repo already has the `llamaagents-NAME` remote.
- `--push` configures the remote and pushes from the current repo.
- `--no-push` skips the push and uses code already available to the server.
Use `llamactl deployments configure-git-remote NAME` to link a repo before relying on auto-push.
## Commands
@@ -88,16 +99,19 @@ llamactl deployments create --no-push
### Edit
```bash
llamactl deployments edit [NAME] [-f FILE] [--no-push] [--project PROJECT]
llamactl deployments edit [NAME] [-f FILE] [--push] [--no-push] [--project PROJECT]
```
Without `-f`, fetches the deployment, renders editable YAML, and opens `$EDITOR`. If `NAME` is omitted in a TTY, choose from existing deployments. Scripts should pass `NAME`.
With `-f FILE`, updates from YAML without opening an editor. If `NAME` is omitted, the YAML must include top-level `name`.
For push-mode deployments, edit auto-pushes only from a repo that already has the `llamaagents-NAME` remote. Pass `--push` to link and push the current repo.
Flags:
- `-f, --filename FILE`: YAML file, or `-` for stdin
- `--push`: Link and push the current repo even if the deployment remote is not configured
- `--no-push`: Skip pushing local code for push-mode deployments
- `--project PROJECT`: Override the active project
@@ -112,17 +126,20 @@ llamactl deployments edit -f deployment.yaml
### Apply
```bash
llamactl deployments apply -f FILE [--dry-run] [--no-push] [--annotate-on-error] [--project PROJECT]
llamactl deployments apply -f FILE [--dry-run] [--push] [--no-push] [--annotate-on-error] [--project PROJECT]
```
Applies deployment YAML declaratively. If the top-level `name` exists, `apply` updates that deployment. If it does not exist, `apply` creates it. YAML produced by `deployments template` or `deployments get NAME -o template` is ready for this command.
`${VAR}` references are resolved from the process environment at apply time. Masked secret values from read output are ignored so round-tripping a deployment does not overwrite existing secrets with placeholders.
For push-mode updates, apply auto-pushes only from a repo that already has the `llamaagents-NAME` remote. Push-mode creates still configure the remote and push by default.
Flags:
- `-f, --filename FILE`: Required YAML file, or `-` for stdin
- `--dry-run`: Validate and print the resolved payload without changing the deployment
- `--push`: Link and push the current repo even if the deployment remote is not configured
- `--no-push`: Skip pushing local code for push-mode deployments
- `--annotate-on-error`: Write validation errors back into the YAML as comments
- `--project PROJECT`: Override the active project
@@ -175,16 +192,17 @@ llamactl deployments delete -f deployment.yaml
### Update
```bash
llamactl deployments update NAME [--git-ref REF] [--no-push] [--project PROJECT]
llamactl deployments update NAME [--git-ref REF] [--push] [--no-push] [--project PROJECT]
```
Resolves the deployment's configured git ref again and starts a new release from the resulting commit. Use `--git-ref` to switch to a branch, tag, or commit before resolving.
For push-mode deployments, `update` mirrors local code before resolving the ref. Use `--no-push` if you already pushed separately or want to redeploy the revision already available to the server.
For push-mode deployments, `update` mirrors local code only when the current repo already has the deployment remote configured. Use `--push` to link and push the current repo, or `--no-push` if you want to redeploy the revision already available to the server.
Flags:
- `--git-ref REF`: Branch, tag, or commit SHA to deploy
- `--push`: Link and push the current repo even if the deployment remote is not configured
- `--no-push`: Skip mirroring local code for push-mode deployments
- `--project PROJECT`: Override the active project
@@ -193,6 +211,7 @@ Examples:
```bash
llamactl deployments update invoice-agent
llamactl deployments update invoice-agent --git-ref release-2026-05
llamactl deployments update invoice-agent --push
llamactl deployments update invoice-agent --no-push
```
@@ -266,6 +285,8 @@ llamactl deployments configure-git-remote NAME [--project PROJECT]
Configures an authenticated git remote for a push-mode deployment. The remote is named `llamaagents-NAME`.
After this runs, `deployments edit`, `deployments apply -f`, and `deployments update` can auto-push from the current repo for that deployment.
Examples:
```bash
@@ -206,9 +206,10 @@ To point at a specific branch, tag, or commit:
llamactl deployments update NAME --git-ref main
```
For push-mode deployments, `update` pushes local code before resolving the ref. If you have already pushed separately, use `--no-push`:
For push-mode deployments, `update` pushes local code only when the current repo already has the deployment remote configured. Use `--push` to link and push the current repo, or `--no-push` to redeploy code already available to the server:
```bash
llamactl deployments update NAME --push
llamactl deployments update NAME --no-push
```
@@ -64,6 +64,7 @@ from ..render import short_sha
from ..utils.git_push import (
configure_git_remote,
get_deployment_git_url,
has_deployment_git_remote,
internal_push_refspec,
push_to_remote,
)
@@ -71,6 +72,7 @@ from ..yaml_template import render as render_yaml_template
DeploymentApplyMode = Literal["apply", "create", "update"]
DeploymentOperationAction = Literal["create", "update"]
PushPolicy = Literal["auto", "always", "never"]
@dataclass(frozen=True)
@@ -532,17 +534,36 @@ def _parse_deployment_yaml_text(text: str) -> DeploymentDisplay:
return parse_apply_yaml(text)
def _push_policy_from_flags(*, push: bool = False, no_push: bool = False) -> PushPolicy:
if push and no_push:
raise click.ClickException("--push and --no-push are mutually exclusive")
if push:
return "always"
if no_push:
return "never"
return "auto"
def _warn_missing_deployment_remote(deployment_id: str) -> None:
remote_name = f"llamaagents-{deployment_id}"
warning(
f"not pushing code; no {remote_name} remote in this repo. "
f"Run llamactl deployments configure-git-remote {deployment_id} "
"or pass --push."
)
async def _apply_deployment_intent(
*,
project: str | None,
intent: _DeploymentIntent,
no_push: bool = False,
push_policy: PushPolicy = "auto",
) -> None:
async with project_client_context(project_id_override=project) as client:
await _apply_deployment_from_yaml(
client,
intent.display,
no_push=no_push,
push_policy=push_policy,
mode=intent.mode,
update_target=intent.update_target,
)
@@ -552,7 +573,7 @@ def _apply_deployment_display(
display: DeploymentDisplay,
*,
project: str | None,
no_push: bool = False,
push_policy: PushPolicy = "auto",
mode: DeploymentApplyMode = "apply",
update_target: str | None = None,
) -> None:
@@ -564,7 +585,7 @@ def _apply_deployment_display(
mode=mode,
update_target=update_target,
),
no_push=no_push,
push_policy=push_policy,
)
)
@@ -573,7 +594,7 @@ def _apply_deployment_yaml_text(
text: str,
*,
project: str | None,
no_push: bool = False,
push_policy: PushPolicy = "auto",
mode: DeploymentApplyMode = "apply",
update_target: str | None = None,
) -> None:
@@ -581,7 +602,7 @@ def _apply_deployment_yaml_text(
_apply_deployment_display(
display,
project=project,
no_push=no_push,
push_policy=push_policy,
mode=mode,
update_target=update_target,
)
@@ -591,7 +612,7 @@ def _apply_deployment_yaml_file(
*,
filename: str,
project: str | None,
no_push: bool,
push_policy: PushPolicy,
mode: DeploymentApplyMode = "apply",
update_target: str | None = None,
) -> None:
@@ -605,7 +626,7 @@ def _apply_deployment_yaml_file(
_apply_deployment_display(
display,
project=project,
no_push=no_push,
push_policy=push_policy,
mode=mode,
update_target=update_target,
)
@@ -662,7 +683,7 @@ def _edit_deployment_yaml_loop(
*,
initial_yaml: str,
project: str | None,
no_push: bool,
push_policy: PushPolicy,
mode: DeploymentApplyMode,
update_target: str | None = None,
) -> None:
@@ -687,7 +708,7 @@ def _edit_deployment_yaml_loop(
_apply_deployment_yaml_text(
current_text,
project=project,
no_push=no_push,
push_policy=push_policy,
mode=mode,
update_target=update_target,
)
@@ -850,11 +871,12 @@ def create_deployment(
project: str | None,
) -> None:
"""Create a new deployment."""
push_policy = _push_policy_from_flags(no_push=no_push)
if filename is not None:
_apply_deployment_yaml_file(
filename=filename,
project=project,
no_push=no_push,
push_policy=push_policy,
mode="create",
)
return
@@ -875,7 +897,7 @@ def create_deployment(
logged_in_email=logged_in_email,
),
project=project,
no_push=no_push,
push_policy=push_policy,
mode="create",
)
@@ -1099,7 +1121,7 @@ async def _execute_deployment_operation(
client: Any,
operation: _ResolvedDeploymentOperation,
*,
no_push: bool = False,
push_policy: PushPolicy = "auto",
) -> None:
display = operation.display
existing = operation.existing
@@ -1155,7 +1177,7 @@ async def _execute_deployment_operation(
push_before_save = current_is_push and desired_is_push
if desired_is_push and no_push:
if desired_is_push and push_policy == "never":
desired_is_push = False
push_before_save = False
@@ -1164,6 +1186,16 @@ async def _execute_deployment_operation(
desired_is_push = False
push_before_save = False
if (
push_before_save
and push_policy == "auto"
and existing is not None
and not has_deployment_git_remote(existing.id)
):
_warn_missing_deployment_remote(existing.id)
desired_is_push = False
push_before_save = False
if push_before_save:
assert existing is not None and display.name is not None
_apply_push(
@@ -1200,7 +1232,7 @@ async def _apply_deployment_from_yaml(
client: Any,
display: DeploymentDisplay,
*,
no_push: bool = False,
push_policy: PushPolicy = "auto",
mode: DeploymentApplyMode = "apply",
update_target: str | None = None,
) -> None:
@@ -1214,7 +1246,7 @@ async def _apply_deployment_from_yaml(
update_target=update_target,
)
operation = await _resolve_deployment_operation(client, intent)
await _execute_deployment_operation(client, operation, no_push=no_push)
await _execute_deployment_operation(client, operation, push_policy=push_policy)
except ApplyYamlError:
raise
except RepositoryValidationError as exc:
@@ -1266,7 +1298,13 @@ async def _apply_deployment_from_yaml(
"--no-push",
is_flag=True,
default=False,
help="Skip pushing local code even when the deployment uses push-mode.",
help="Skip pushing local code for push-mode deployments.",
)
@click.option(
"--push",
is_flag=True,
default=False,
help="Push local code even if this repo is not linked to the deployment.",
)
@click.option(
"--annotate-on-error",
@@ -1279,6 +1317,7 @@ def apply_deployment(
filename: str,
dry_run: bool,
no_push: bool,
push: bool,
annotate_on_error: bool,
project: str | None,
) -> None:
@@ -1288,6 +1327,7 @@ def apply_deployment(
Reads the file (or stdin with ``-f -``), resolves ``${VAR}`` references
from the environment, and issues the appropriate API call.
"""
push_policy = _push_policy_from_flags(push=push, no_push=no_push)
text = _read_apply_input(filename)
try:
display = _parse_deployment_yaml_text(text)
@@ -1322,7 +1362,11 @@ def apply_deployment(
return
try:
_apply_deployment_display(display, project=project, no_push=no_push)
_apply_deployment_display(
display,
project=project,
push_policy=push_policy,
)
except ApplyYamlError as exc:
if annotate_on_error:
_handle_annotated_apply_error(
@@ -1347,21 +1391,29 @@ def apply_deployment(
"--no-push",
is_flag=True,
default=False,
help="Skip pushing local code even when the deployment uses push-mode.",
help="Skip pushing local code for push-mode deployments.",
)
@click.option(
"--push",
is_flag=True,
default=False,
help="Push local code even if this repo is not linked to the deployment.",
)
@project_option
def edit_deployment(
deployment_id: str | None,
filename: str | None,
no_push: bool,
push: bool,
project: str | None,
) -> None:
"""Edit a deployment in $EDITOR."""
push_policy = _push_policy_from_flags(push=push, no_push=no_push)
if filename is not None:
_apply_deployment_yaml_file(
filename=filename,
project=project,
no_push=no_push,
push_policy=push_policy,
mode="update",
update_target=deployment_id,
)
@@ -1380,7 +1432,7 @@ def edit_deployment(
_edit_deployment_yaml_loop(
initial_yaml=_existing_deployment_editor_yaml(current_deployment),
project=project,
no_push=no_push,
push_policy=push_policy,
mode="update",
update_target=deployment_id,
)
@@ -1399,6 +1451,7 @@ def _push_internal_for_update(
deployment_id: str,
git_ref: str | None,
client: Any,
push_policy: PushPolicy,
) -> None:
"""Push local code to the internal repo before updating.
@@ -1409,6 +1462,10 @@ def _push_internal_for_update(
warning("not in a git repo; skipping push, server will use last pushed code")
return
if push_policy == "auto" and not has_deployment_git_remote(deployment_id):
_warn_missing_deployment_remote(deployment_id)
return
git_url = get_deployment_git_url(client.base_url, deployment_id)
remote_name = configure_git_remote(
git_url, client.api_key, client.project_id, deployment_id
@@ -1443,14 +1500,22 @@ def _push_internal_for_update(
default=False,
help="Skip pushing local code for internal-repo deployments.",
)
@click.option(
"--push",
is_flag=True,
default=False,
help="Push local code even if this repo is not linked to the deployment.",
)
@project_option
def refresh_deployment(
deployment_id: str | None,
git_ref: str | None,
no_push: bool,
push: bool,
project: str | None,
) -> None:
"""Update the deployment, pulling the latest code from its branch."""
push_policy = _push_policy_from_flags(push=push, no_push=no_push)
deployment_id = _require_deployment_id(deployment_id, "update", project)
try:
# Single asyncio.run with one client: reusing a ProjectClient across
@@ -1460,11 +1525,15 @@ def refresh_deployment(
async with project_client_context(project_id_override=project) as client:
current = await client.get_deployment(deployment_id)
effective_git_ref = git_ref or current.git_ref
if current.repo_url == INTERNAL_CODE_REPO_SCHEME and not no_push:
if (
current.repo_url == INTERNAL_CODE_REPO_SCHEME
and push_policy != "never"
):
_push_internal_for_update(
deployment_id,
effective_git_ref,
client=client,
push_policy=push_policy,
)
# Re-resolves the branch to the latest commit SHA on the server.
status(f"refreshing {deployment_id}")
@@ -11,6 +11,15 @@ def _git_remote_name(deployment_id: str) -> str:
return f"llamaagents-{deployment_id}"
def has_deployment_git_remote(deployment_id: str) -> bool:
"""Return whether the standard deployment git remote exists."""
result = subprocess.run(
["git", "remote", "get-url", _git_remote_name(deployment_id)],
capture_output=True,
)
return result.returncode == 0
def get_deployment_git_url(base_url: str, deployment_id: str) -> str:
"""Build the git endpoint URL for a deployment."""
api_url = base_url.rstrip("/")
@@ -1122,6 +1122,7 @@ def _patched_git_push(
Yields the ``push_to_remote`` mock for assertions.
"""
with (
patch(f"{_DEPLOY_CMD}.has_deployment_git_remote", return_value=True),
patch(f"{_DEPLOY_CMD}.configure_git_remote", return_value="llamaagents-test"),
patch(
f"{_DEPLOY_CMD}.push_to_remote",
@@ -1288,7 +1289,7 @@ def test_apply_push_mode_create_does_save_then_push(
def test_apply_push_mode_update_does_push_then_save(
patched_auth: Any, tmp_path: Any
) -> None:
"""Existing push-mode + desired push-mode → push first, then save."""
"""Existing push-mode + linked repo → push first, then save."""
runner = CliRunner()
f = tmp_path / "deploy.yaml"
f.write_text("name: my-app\nspec:\n git_ref: feature-branch\n")
@@ -1304,6 +1305,79 @@ def test_apply_push_mode_update_does_push_then_save(
client.update_deployment.assert_called_once()
def test_apply_push_mode_update_skips_push_when_remote_missing(
patched_auth: Any, tmp_path: Any
) -> None:
"""Existing push-mode + unlinked repo → save without pushing."""
runner = CliRunner()
f = tmp_path / "deploy.yaml"
f.write_text("name: my-app\nspec:\n git_ref: feature-branch\n")
client = _push_mode_client()
with (
patch_project_client(client),
patch(f"{_DEPLOY_CMD}.is_git_repo", return_value=True),
patch(f"{_DEPLOY_CMD}.has_deployment_git_remote", return_value=False),
patch(f"{_DEPLOY_CMD}.configure_git_remote") as configure,
patch(f"{_DEPLOY_CMD}.push_to_remote") as push,
):
result = runner.invoke(app, ["deployments", "apply", "-f", str(f)])
assert result.exit_code == 0, result.output
assert "updated my-app" in result.output
assert "warning: not pushing code; no llamaagents-my-app remote" in result.stderr
configure.assert_not_called()
push.assert_not_called()
client.update_deployment.assert_called_once()
def test_apply_push_mode_update_push_flag_forces_push(
patched_auth: Any, tmp_path: Any
) -> None:
"""``--push`` configures and pushes even when the remote is missing."""
runner = CliRunner()
f = tmp_path / "deploy.yaml"
f.write_text("name: my-app\nspec:\n git_ref: feature-branch\n")
client = _push_mode_client()
with (
patch_project_client(client),
patch(f"{_DEPLOY_CMD}.is_git_repo", return_value=True),
patch(f"{_DEPLOY_CMD}.has_deployment_git_remote", return_value=False),
patch(f"{_DEPLOY_CMD}.configure_git_remote", return_value="llamaagents-test"),
patch(
f"{_DEPLOY_CMD}.push_to_remote",
return_value=subprocess.CompletedProcess([], 0, stderr=b""),
) as push,
):
result = runner.invoke(app, ["deployments", "apply", "-f", str(f), "--push"])
assert result.exit_code == 0, result.output
assert "updated my-app" in result.output
push.assert_called_once()
client.update_deployment.assert_called_once()
def test_apply_push_and_no_push_are_mutually_exclusive(
patched_auth: Any, tmp_path: Any
) -> None:
runner = CliRunner()
f = tmp_path / "deploy.yaml"
f.write_text("name: my-app\nspec:\n git_ref: feature-branch\n")
client = _push_mode_client()
with patch_project_client(client):
result = runner.invoke(
app,
["deployments", "apply", "-f", str(f), "--push", "--no-push"],
)
assert result.exit_code != 0
assert "--push and --no-push are mutually exclusive" in result.output
client.get_deployment.assert_not_called()
client.update_deployment.assert_not_called()
def test_apply_push_then_save_push_failure_aborts(
patched_auth: Any, tmp_path: Any
) -> None:
@@ -372,6 +372,33 @@ def test_edit_opens_current_template_and_updates_saved_yaml(patched_auth: Any) -
assert "updated my-app" in result.output
def test_edit_push_mode_skips_push_when_remote_missing(patched_auth: Any) -> None:
runner = CliRunner()
existing = make_deployment("my-app", repo_url="internal://", git_ref="main")
client = _editor_client_mock(existing=existing)
saved_text = textwrap.dedent("""\
name: my-app
spec:
git_ref: v2
""")
with (
patch_project_client(client),
_patch_yaml_editor(saved_text),
patch(f"{_DEPLOY_CMD}.is_git_repo", return_value=True),
patch(f"{_DEPLOY_CMD}.has_deployment_git_remote", return_value=False),
patch(f"{_DEPLOY_CMD}.configure_git_remote") as configure,
patch(f"{_DEPLOY_CMD}.push_to_remote") as push,
):
result = runner.invoke(app, ["deployments", "edit", "my-app"])
assert result.exit_code == 0, result.output
assert "warning: not pushing code; no llamaagents-my-app remote" in result.stderr
configure.assert_not_called()
push.assert_not_called()
client.update_deployment.assert_called_once()
def test_edit_preserves_existing_secret_names_as_masks(patched_auth: Any) -> None:
runner = CliRunner()
existing = make_deployment("my-app", secret_names=["MY_SECRET"])
@@ -404,6 +431,57 @@ def test_edit_file_uses_update_intent_not_create(
assert client.update_deployment.call_args[0][0] == "my-app"
def test_edit_file_push_mode_skips_push_when_remote_missing(
patched_auth: Any, tmp_path: Path
) -> None:
runner = CliRunner()
f = tmp_path / "deploy.yaml"
f.write_text("name: my-app\nspec:\n git_ref: v2\n")
client = _editor_client_mock(
existing=make_deployment("my-app", repo_url="internal://")
)
with (
patch_project_client(client),
patch(f"{_DEPLOY_CMD}.is_git_repo", return_value=True),
patch(f"{_DEPLOY_CMD}.has_deployment_git_remote", return_value=False),
patch(f"{_DEPLOY_CMD}.configure_git_remote") as configure,
patch(f"{_DEPLOY_CMD}.push_to_remote") as push,
):
result = runner.invoke(app, ["deployments", "edit", "-f", str(f)])
assert result.exit_code == 0, result.output
assert "warning: not pushing code; no llamaagents-my-app remote" in result.stderr
configure.assert_not_called()
push.assert_not_called()
client.update_deployment.assert_called_once()
def test_edit_file_push_flag_forces_push(patched_auth: Any, tmp_path: Path) -> None:
runner = CliRunner()
f = tmp_path / "deploy.yaml"
f.write_text("name: my-app\nspec:\n git_ref: v2\n")
client = _editor_client_mock(
existing=make_deployment("my-app", repo_url="internal://")
)
with (
patch_project_client(client),
patch(f"{_DEPLOY_CMD}.is_git_repo", return_value=True),
patch(f"{_DEPLOY_CMD}.has_deployment_git_remote", return_value=False),
patch(f"{_DEPLOY_CMD}.configure_git_remote", return_value="llamaagents-test"),
patch(
f"{_DEPLOY_CMD}.push_to_remote",
return_value=subprocess.CompletedProcess([], 0, stderr=b""),
) as push,
):
result = runner.invoke(app, ["deployments", "edit", "-f", str(f), "--push"])
assert result.exit_code == 0, result.output
push.assert_called_once()
client.update_deployment.assert_called_once()
def test_interactive_edit_uses_separate_clients_for_fetch_and_apply(
patched_auth: Any,
) -> None:
@@ -94,6 +94,10 @@ def test_deployments_update_internal_repo_push_failure_does_not_abort(
with (
patch_project_client(client),
patch("llama_agents.cli.commands.deployment.is_git_repo", return_value=True),
patch(
"llama_agents.cli.commands.deployment.has_deployment_git_remote",
return_value=True,
),
patch(
"llama_agents.cli.commands.deployment.configure_git_remote",
return_value="llamaagents-my-app",
@@ -121,6 +125,78 @@ def test_deployments_update_internal_repo_push_failure_does_not_abort(
client.update_deployment.assert_called_once()
def test_deployments_update_internal_repo_skips_push_when_remote_missing(
patched_auth: Any,
) -> None:
runner = CliRunner()
current = make_deployment(
"my-app", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha="a" * 40
)
updated = make_deployment(
"my-app", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha="b" * 40
)
client = _client_mock(current, updated)
with (
patch_project_client(client),
patch("llama_agents.cli.commands.deployment.is_git_repo", return_value=True),
patch(
"llama_agents.cli.commands.deployment.has_deployment_git_remote",
return_value=False,
),
patch(
"llama_agents.cli.commands.deployment.configure_git_remote"
) as configure_git_remote,
patch("llama_agents.cli.commands.deployment.push_to_remote") as push_to_remote,
):
result = runner.invoke(app, ["deployments", "update", "my-app"])
assert result.exit_code == 0, result.output
assert result.stdout == ""
assert "warning: not pushing code; no llamaagents-my-app remote" in result.stderr
assert "refreshing my-app" in result.stderr
configure_git_remote.assert_not_called()
push_to_remote.assert_not_called()
client.get_deployment.assert_called_once()
client.update_deployment.assert_called_once()
def test_deployments_update_push_flag_forces_internal_git_push(
patched_auth: Any,
) -> None:
runner = CliRunner()
current = make_deployment(
"my-app", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha="a" * 40
)
updated = make_deployment(
"my-app", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha="b" * 40
)
client = _client_mock(current, updated)
with (
patch_project_client(client),
patch("llama_agents.cli.commands.deployment.is_git_repo", return_value=True),
patch(
"llama_agents.cli.commands.deployment.has_deployment_git_remote",
return_value=False,
),
patch(
"llama_agents.cli.commands.deployment.configure_git_remote",
return_value="llamaagents-my-app",
) as configure_git_remote,
patch(
"llama_agents.cli.commands.deployment.push_to_remote",
return_value=_completed_process(),
) as push_to_remote,
):
result = runner.invoke(app, ["deployments", "update", "my-app", "--push"])
assert result.exit_code == 0, result.output
configure_git_remote.assert_called_once()
push_to_remote.assert_called_once()
client.update_deployment.assert_called_once()
def test_deployments_update_no_push_skips_internal_git_push(
patched_auth: Any,
) -> None:
@@ -152,3 +228,26 @@ def test_deployments_update_no_push_skips_internal_git_push(
push_to_remote.assert_not_called()
client.get_deployment.assert_called_once()
client.update_deployment.assert_called_once()
def test_deployments_update_push_and_no_push_are_mutually_exclusive(
patched_auth: Any,
) -> None:
runner = CliRunner()
current = make_deployment(
"my-app", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha="a" * 40
)
updated = make_deployment(
"my-app", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha="b" * 40
)
client = _client_mock(current, updated)
with patch_project_client(client):
result = runner.invoke(
app, ["deployments", "update", "my-app", "--push", "--no-push"]
)
assert result.exit_code != 0
assert "--push and --no-push are mutually exclusive" in result.output
client.get_deployment.assert_not_called()
client.update_deployment.assert_not_called()
+23
View File
@@ -12,6 +12,7 @@ from llama_agents.cli.utils.git_push import (
configure_git_remote,
get_api_key,
get_deployment_git_url,
has_deployment_git_remote,
push_to_remote,
)
@@ -34,6 +35,28 @@ def test_get_deployment_git_url_strips_trailing_slash() -> None:
assert url == "http://localhost:8000/api/v1beta1/deployments/dep-1/git"
@patch("llama_agents.cli.utils.git_push.subprocess")
def test_has_deployment_git_remote_returns_true_when_remote_exists(
mock_subprocess: MagicMock,
) -> None:
mock_subprocess.run.return_value = MagicMock(returncode=0)
assert has_deployment_git_remote("dep-1") is True
mock_subprocess.run.assert_called_once_with(
["git", "remote", "get-url", "llamaagents-dep-1"],
capture_output=True,
)
@patch("llama_agents.cli.utils.git_push.subprocess")
def test_has_deployment_git_remote_returns_false_when_remote_missing(
mock_subprocess: MagicMock,
) -> None:
mock_subprocess.run.return_value = MagicMock(returncode=2)
assert has_deployment_git_remote("dep-1") is False
# ---------------------------------------------------------------------------
# get_api_key
# ---------------------------------------------------------------------------