mirror of
https://github.com/run-llama/workflows-py.git
synced 2026-07-20 19:57:02 -04:00
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:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user