Fix Jenkins-update flow: redeploy_project pull + delete_image safety + delete_container

Bug fixes from product test:
1. redeploy_project: BUILD_FAILED now includes explicit image pull (stop → pull → start)
2. delete_image: Distinguishes running vs stopped containers, suggests system_prune for stopped refs
3. New tool delete_container: Verify stopped state before deletion, confirmation required

Tests added for all three paths plus stopped-container edge cases.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 07:09:21 +02:00
parent 81d5acd83e
commit 223075e602
7 changed files with 207 additions and 26 deletions
@@ -312,6 +312,58 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N
return "\n".join(result_lines)
@mcp.tool()
async def delete_container(container_name: str, confirmed: bool = False) -> str:
"""Delete a container.
Container must be stopped before deletion. Without confirmed=True,
returns a preview only.
Args:
container_name: Name of the container.
confirmed: Must be True to proceed. Set to True to confirm deletion.
"""
if not confirmed:
return (
f"Preview: would delete container '{container_name}'.\n"
f"Call this tool again with confirmed=True to proceed."
)
resolved_name = await _resolve_container_name(client, container_name)
display_name = _strip_hash_prefix(resolved_name)
try:
data = await client.request(
"SYNO.Docker.Container",
"get",
params={"name": resolved_name},
)
except Exception as e:
return f"Error inspecting container '{container_name}': {e}"
if not data:
return f"Container '{container_name}' not found."
details = data.get("details", {}) or {}
state = details.get("State", {}) or {}
running = state.get("Running", False)
if running:
return (
f"Cannot delete '{display_name}': container is still running.\n"
f"Stop the container first with stop_project or stop_container."
)
try:
await client.request(
"SYNO.Docker.Container",
"delete",
params={"name": resolved_name},
)
return f"Deleted container '{display_name}'."
except Exception as e:
return f"Error deleting '{display_name}': {e}"
def _container_in_project(container: dict[str, Any], project_name: str) -> bool:
"""Check if a container belongs to a project based on its labels."""
@@ -350,9 +402,7 @@ def _format_container_detail(name: str, data: dict[str, Any]) -> str:
lines.append(f" Exit code: {state.get('ExitCode', '?')}")
# IP addresses from all attached networks
networks: dict[str, Any] = (
details.get("NetworkSettings", {}) or {}
).get("Networks", {}) or {}
networks: dict[str, Any] = (details.get("NetworkSettings", {}) or {}).get("Networks", {}) or {}
for net_name, net in networks.items():
ip = (net or {}).get("IPAddress", "")
if ip: