diff --git a/CHANGELOG.md b/CHANGELOG.md index 6639d73..08864b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 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 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 09a28df..9857ca4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcp-synology-container" -version = "0.2.1" +version = "0.2.2" description = "MCP server for Synology Container Manager" requires-python = ">=3.12" dependencies = [ diff --git a/src/mcp_synology_container/modules/projects.py b/src/mcp_synology_container/modules/projects.py index 99312a7..f1b0b79 100644 --- a/src/mcp_synology_container/modules/projects.py +++ b/src/mcp_synology_container/modules/projects.py @@ -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}) results.append(" Build stopped.") results.append("Step 2/4: Pulling updated images...") - with contextlib.suppress(Exception): + try: 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("Step 3/4: Starting project...") await client.request("SYNO.Docker.Project", "start", params={"id": project_id}) diff --git a/tests/test_modules/test_projects.py b/tests/test_modules/test_projects.py index 967f7c0..fd8439c 100644 --- a/tests/test_modules/test_projects.py +++ b/tests/test_modules/test_projects.py @@ -286,13 +286,13 @@ async def test_redeploy_build_failed_project(): @pytest.mark.asyncio 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 client, _ = make_stateful_redeploy_mock( "BUILD_FAILED", stop_raises=SynologyError("already stopped", code=2101), - pull_raises=SynologyError("pull failed", code=2102), + pull_raises=None, # pull succeeds ) tools = make_projects_tools(client) @@ -302,6 +302,29 @@ async def test_redeploy_build_failed_stop_error_nonfatal(): 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 async def test_redeploy_poll_timeout(): """If project never reaches RUNNING after start, a warning is emitted.""" diff --git a/uv.lock b/uv.lock index ce258e7..80f76da 100644 --- a/uv.lock +++ b/uv.lock @@ -362,7 +362,7 @@ wheels = [ [[package]] name = "mcp-synology-container" -version = "0.2.1" +version = "0.2.2" source = { editable = "." } dependencies = [ { name = "click" },