v0.2.2: BUILD_FAILED pull failure aborts redeploy with clear message
Remove contextlib.suppress from the image pull step in the BUILD_FAILED redeploy path. A failed pull (e.g. non-existent tag) now immediately returns an actionable error pointing to update_image_tag instead of silently continuing and starting the project with stale/missing image. Also bumps version 0.2.1 → 0.2.2 and adds CHANGELOG entry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.2.2] - 2026-04-21
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `redeploy_project` (BUILD_FAILED): Pull errors are no longer silently suppressed.
|
||||||
|
If the image pull fails (e.g. the tag in compose.yaml does not exist on the registry),
|
||||||
|
redeploy aborts immediately with a clear message pointing to `update_image_tag`.
|
||||||
|
|
||||||
## [0.2.1] - 2026-04-21
|
## [0.2.1] - 2026-04-21
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mcp-synology-container"
|
name = "mcp-synology-container"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
description = "MCP server for Synology Container Manager"
|
description = "MCP server for Synology Container Manager"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -167,8 +167,15 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non
|
|||||||
await client.request("SYNO.Docker.Project", "stop", params={"id": project_id})
|
await client.request("SYNO.Docker.Project", "stop", params={"id": project_id})
|
||||||
results.append(" Build stopped.")
|
results.append(" Build stopped.")
|
||||||
results.append("Step 2/4: Pulling updated images...")
|
results.append("Step 2/4: Pulling updated images...")
|
||||||
with contextlib.suppress(Exception):
|
try:
|
||||||
await client.request("SYNO.Docker.Image", "pull", params={"id": project_id})
|
await client.request("SYNO.Docker.Image", "pull", params={"id": project_id})
|
||||||
|
except Exception as pull_err:
|
||||||
|
results.append(f" Pull failed: {pull_err}")
|
||||||
|
results.append(
|
||||||
|
"Aborted: image pull failed — the image tag in compose.yaml may not exist. "
|
||||||
|
"Fix the tag with update_image_tag, then retry redeploy_project."
|
||||||
|
)
|
||||||
|
return "\n".join(results)
|
||||||
results.append(" Images pulled.")
|
results.append(" Images pulled.")
|
||||||
results.append("Step 3/4: Starting project...")
|
results.append("Step 3/4: Starting project...")
|
||||||
await client.request("SYNO.Docker.Project", "start", params={"id": project_id})
|
await client.request("SYNO.Docker.Project", "start", params={"id": project_id})
|
||||||
|
|||||||
@@ -286,13 +286,13 @@ async def test_redeploy_build_failed_project():
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_redeploy_build_failed_stop_error_nonfatal():
|
async def test_redeploy_build_failed_stop_error_nonfatal():
|
||||||
"""BUILD_FAILED: stop/pull failures must not abort the redeploy."""
|
"""BUILD_FAILED: stop failure is non-fatal and must not abort the redeploy."""
|
||||||
from mcp_synology_container.dsm_client import SynologyError
|
from mcp_synology_container.dsm_client import SynologyError
|
||||||
|
|
||||||
client, _ = make_stateful_redeploy_mock(
|
client, _ = make_stateful_redeploy_mock(
|
||||||
"BUILD_FAILED",
|
"BUILD_FAILED",
|
||||||
stop_raises=SynologyError("already stopped", code=2101),
|
stop_raises=SynologyError("already stopped", code=2101),
|
||||||
pull_raises=SynologyError("pull failed", code=2102),
|
pull_raises=None, # pull succeeds
|
||||||
)
|
)
|
||||||
tools = make_projects_tools(client)
|
tools = make_projects_tools(client)
|
||||||
|
|
||||||
@@ -302,6 +302,29 @@ async def test_redeploy_build_failed_stop_error_nonfatal():
|
|||||||
assert "redeployed successfully" in result
|
assert "redeployed successfully" in result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_redeploy_build_failed_pull_error_aborts():
|
||||||
|
"""BUILD_FAILED: pull failure must abort redeploy with a clear message."""
|
||||||
|
from mcp_synology_container.dsm_client import SynologyError
|
||||||
|
|
||||||
|
client, calls = make_stateful_redeploy_mock(
|
||||||
|
"BUILD_FAILED",
|
||||||
|
stop_raises=None,
|
||||||
|
pull_raises=SynologyError("image not found", code=114),
|
||||||
|
)
|
||||||
|
tools = make_projects_tools(client)
|
||||||
|
|
||||||
|
with patch("mcp_synology_container.modules.projects.asyncio.sleep"):
|
||||||
|
result = await tools["redeploy_project"]("myapp", confirmed=True)
|
||||||
|
|
||||||
|
assert "redeployed successfully" not in result
|
||||||
|
assert "Aborted" in result or "pull failed" in result.lower()
|
||||||
|
assert "compose.yaml" in result or "update_image_tag" in result
|
||||||
|
# start must NOT have been called after a pull failure
|
||||||
|
methods = [m for _, m in calls]
|
||||||
|
assert "start" not in methods
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_redeploy_poll_timeout():
|
async def test_redeploy_poll_timeout():
|
||||||
"""If project never reaches RUNNING after start, a warning is emitted."""
|
"""If project never reaches RUNNING after start, a warning is emitted."""
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mcp-synology-container"
|
name = "mcp-synology-container"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
|
|||||||
Reference in New Issue
Block a user