From 903b90ef2d5dc03737cca7ef2df9c9625062bf10 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:25:12 +0000 Subject: [PATCH] fix(docker-compose): Accept any Compose at or above v2.29.7 The version check required an exact match, so after the bump to v5.6.0 every developer with a different Compose (including Docker Desktop's bundled one) would have it replaced on their next devservices run. Treat MINIMUM_DOCKER_COMPOSE_VERSION as a real minimum (v2.29.7, the version devservices pinned until #332) and only install DOCKER_COMPOSE_INSTALL_VERSION (v5.6.0) when Compose is missing, unparseable or older than the minimum. Co-Authored-By: Joshua Li <14209781+joshuarli@users.noreply.github.com> --- devservices/constants.py | 5 ++- devservices/utils/docker_compose.py | 47 +++++++++-------------------- tests/utils/test_docker_compose.py | 28 ++++++++++++++++- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/devservices/constants.py b/devservices/constants.py index f08279a..d8fb609 100644 --- a/devservices/constants.py +++ b/devservices/constants.py @@ -22,7 +22,10 @@ class DependencyType(StrEnum): SUPERVISOR = "supervisor" -MINIMUM_DOCKER_COMPOSE_VERSION = "5.6.0" +# Any installed Docker Compose at or above the minimum is used as is. The install +# version is only downloaded when Compose is missing or older than the minimum. +MINIMUM_DOCKER_COMPOSE_VERSION = "2.29.7" +DOCKER_COMPOSE_INSTALL_VERSION = "5.6.0" DEVSERVICES_DIR_NAME = "devservices" CONFIG_FILE_NAME = "config.yml" DOCKER_CONFIG_DIR = os.environ.get("DOCKER_CONFIG", os.path.expanduser("~/.docker")) diff --git a/devservices/utils/docker_compose.py b/devservices/utils/docker_compose.py index 04ac3a2..82138d6 100644 --- a/devservices/utils/docker_compose.py +++ b/devservices/utils/docker_compose.py @@ -10,7 +10,6 @@ import subprocess import time from typing import NamedTuple -from typing import cast from packaging import version from sentry_sdk import logger as sentry_logger @@ -19,6 +18,7 @@ from devservices.constants import CONFIG_FILE_NAME from devservices.constants import DEVSERVICES_DIR_NAME from devservices.constants import DOCKER_COMPOSE_DOWNLOAD_URL +from devservices.constants import DOCKER_COMPOSE_INSTALL_VERSION from devservices.constants import DOCKER_USER_PLUGIN_DIR from devservices.constants import LOGGER_NAME from devservices.constants import MINIMUM_DOCKER_COMPOSE_VERSION @@ -69,7 +69,7 @@ def install_docker_compose() -> None: else: raise DockerComposeInstallationError(f"Unsupported operating system: {system}") - url = f"{DOCKER_COMPOSE_DOWNLOAD_URL}/v{MINIMUM_DOCKER_COMPOSE_VERSION}/{binary_name_with_extension}" + url = f"{DOCKER_COMPOSE_DOWNLOAD_URL}/v{DOCKER_COMPOSE_INSTALL_VERSION}/{binary_name_with_extension}" destination = os.path.join(DOCKER_USER_PLUGIN_DIR, binary_name) os.makedirs(DOCKER_USER_PLUGIN_DIR, exist_ok=True) @@ -77,14 +77,14 @@ def install_docker_compose() -> None: install_binary( binary_name, destination, - MINIMUM_DOCKER_COMPOSE_VERSION, + DOCKER_COMPOSE_INSTALL_VERSION, url, ) except BinaryInstallError as e: raise DockerComposeInstallationError(f"Failed to install Docker Compose: {e}") console.success( - f"Docker Compose {MINIMUM_DOCKER_COMPOSE_VERSION} installed successfully to {destination}" + f"Docker Compose {DOCKER_COMPOSE_INSTALL_VERSION} installed successfully to {destination}" ) # Verify the installation @@ -138,42 +138,25 @@ def check_docker_compose_version() -> None: # Throw an error if docker daemon isn't running check_docker_daemon_running() try: - # Run the docker compose version command - docker_compose_version = get_docker_compose_version() + version_output = get_docker_compose_version() except DockerComposeError: - docker_compose_version = None console.warning( - f"Docker Compose is not installed, attempting to install v{MINIMUM_DOCKER_COMPOSE_VERSION}" + f"Docker Compose is not installed, attempting to install v{DOCKER_COMPOSE_INSTALL_VERSION}" ) + install_docker_compose() + return - # Extract the version number from the output - version_output = ( - docker_compose_version if docker_compose_version is not None else "" - ) - - # Use regex to find the version number - pattern = r"^(\d+\.\d+\.\d+)" - - match = re.search(pattern, version_output) - if match: - # There is a chance that Any type is returned, so cast this - docker_compose_version = cast(str, match.group(1)) - else: - docker_compose_version = None - - if docker_compose_version is None: + # Docker Desktop reports versions like "2.29.7-desktop.1", so only match the prefix + match = re.search(r"^(\d+\.\d+\.\d+)", version_output) + if match is None: console.warning( - f"Unable to detect Docker Compose version, attempting to install v{MINIMUM_DOCKER_COMPOSE_VERSION}" + f"Unable to detect Docker Compose version, attempting to install v{DOCKER_COMPOSE_INSTALL_VERSION}" ) - elif version.parse(docker_compose_version) != version.parse( - MINIMUM_DOCKER_COMPOSE_VERSION - ): + elif version.parse(match.group(1)) < version.parse(MINIMUM_DOCKER_COMPOSE_VERSION): console.warning( - f"Docker Compose version v{docker_compose_version} unsupported, attempting to install v{MINIMUM_DOCKER_COMPOSE_VERSION}" + f"Docker Compose v{match.group(1)} is older than the minimum supported v{MINIMUM_DOCKER_COMPOSE_VERSION}, attempting to install v{DOCKER_COMPOSE_INSTALL_VERSION}" ) - elif version.parse(docker_compose_version) == version.parse( - MINIMUM_DOCKER_COMPOSE_VERSION - ): + else: return install_docker_compose() diff --git a/tests/utils/test_docker_compose.py b/tests/utils/test_docker_compose.py index ab6dd2f..3d6d957 100644 --- a/tests/utils/test_docker_compose.py +++ b/tests/utils/test_docker_compose.py @@ -83,7 +83,33 @@ def test_check_docker_compose_version_unsupported( ) -> None: mock_run.return_value.stdout = "2.20.0-desktop.1\n" check_docker_compose_version() - assert mock_install_docker_compose.is_called() + mock_install_docker_compose.assert_called_once() + + +@pytest.mark.parametrize( + "installed_version", + ["2.29.7", "2.29.7-desktop.1", "2.38.2", "2.40.3-desktop.1", "5.6.0", "6.0.0"], +) +@mock.patch("subprocess.run") +@mock.patch("devservices.utils.docker_compose.install_docker_compose") +def test_check_docker_compose_version_at_or_above_minimum( + mock_install_docker_compose: mock.Mock, + mock_run: mock.Mock, + installed_version: str, +) -> None: + mock_run.return_value.stdout = f"{installed_version}\n" + check_docker_compose_version() + mock_install_docker_compose.assert_not_called() + + +@mock.patch("subprocess.run") +@mock.patch("devservices.utils.docker_compose.install_docker_compose") +def test_check_docker_compose_version_below_minimum( + mock_install_docker_compose: mock.Mock, mock_run: mock.Mock +) -> None: + mock_run.return_value.stdout = "2.29.6\n" + check_docker_compose_version() + mock_install_docker_compose.assert_called_once() @mock.patch("subprocess.run")