- Docker update detection for in-app updates

- Added _is_docker_environment() function
  - Check endpoint returns is_docker and update_method fields
  - Apply endpoint rejects Docker with helpful instructions
- Added updates API tests
- Updated CHANGELOG

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
maziggy
2026-01-01 14:06:06 +01:00
co-authored by Claude Opus 4.5
parent 7e3fcec02d
commit 1234e44eb2
3 changed files with 77 additions and 0 deletions
+1
View File
@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
## [0.1.6b4] - 2026-01-01
### Added
- **Docker update detection** - Automatically detects Docker installations and shows appropriate update instructions instead of failing with git errors
- **Camera popup window improvements**
- Auto-resize to fit video resolution on first open
- Persist window size and position to localStorage
+29
View File
@@ -27,6 +27,20 @@ _update_status = {
}
def _is_docker_environment() -> bool:
"""Detect if running inside a Docker container."""
if os.path.exists("/.dockerenv"):
return True
try:
with open("/proc/1/cgroup") as f:
if "docker" in f.read():
return True
except (FileNotFoundError, PermissionError):
pass
git_dir = settings.base_dir / ".git"
return not git_dir.exists()
def _find_executable(name: str) -> str | None:
"""Find an executable in PATH or common locations."""
# Try standard PATH first
@@ -199,6 +213,7 @@ async def check_for_updates(db: AsyncSession = Depends(get_db)):
"error": None,
}
is_docker = _is_docker_environment()
return {
"update_available": update_available,
"current_version": APP_VERSION,
@@ -207,6 +222,8 @@ async def check_for_updates(db: AsyncSession = Depends(get_db)):
"release_notes": release_notes,
"release_url": release_url,
"published_at": published_at,
"is_docker": is_docker,
"update_method": "docker" if is_docker else "git",
}
except httpx.HTTPError as e:
@@ -426,6 +443,18 @@ async def apply_update(background_tasks: BackgroundTasks):
"status": _update_status,
}
# Check if running in Docker
if _is_docker_environment():
return {
"success": False,
"is_docker": True,
"message": (
"Docker installations cannot be updated in-app. "
"Please update via Docker Compose: "
"git pull && docker compose build --pull && docker compose up -d"
),
}
# Start update in background
background_tasks.add_task(_perform_update)
@@ -0,0 +1,47 @@
"""Integration tests for Updates API endpoints."""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from httpx import AsyncClient
class TestUpdatesAPI:
@pytest.mark.asyncio
async def test_get_version(self, async_client: AsyncClient):
response = await async_client.get("/api/v1/updates/version")
assert response.status_code == 200
@pytest.mark.asyncio
async def test_apply_update_docker_rejection(self, async_client: AsyncClient):
with patch("backend.app.api.routes.updates._is_docker_environment", return_value=True):
response = await async_client.post("/api/v1/updates/apply")
result = response.json()
assert result["success"] is False
assert result["is_docker"] is True
@pytest.mark.asyncio
async def test_apply_update_non_docker(self, async_client: AsyncClient):
"""Test non-Docker path - mock _perform_update to prevent side effects."""
with (
patch("backend.app.api.routes.updates._is_docker_environment", return_value=False),
patch("backend.app.api.routes.updates._perform_update", new_callable=AsyncMock),
):
response = await async_client.post("/api/v1/updates/apply")
assert response.json()["success"] is True
def test_is_docker_with_dockerenv(self):
from backend.app.api.routes.updates import _is_docker_environment
with patch("os.path.exists", return_value=True):
assert _is_docker_environment() is True
def test_parse_version(self):
from backend.app.api.routes.updates import parse_version
assert parse_version("0.1.5")[:3] == (0, 1, 5)
def test_is_newer_version(self):
from backend.app.api.routes.updates import is_newer_version
assert is_newer_version("0.1.5", "0.1.5b7") is True