This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new b9172bf1003 Stop breeze rewriting uv.lock when forcing lowest 
dependencies (#73305)
b9172bf1003 is described below

commit b9172bf1003eebfd7509e8b827449577f986b966
Author: Jarek Potiuk <[email protected]>
AuthorDate: Tue Sep 22 17:33:58 2026 +0200

    Stop breeze rewriting uv.lock when forcing lowest dependencies (#73305)
    
    Running breeze with --force-lowest-dependencies re-resolves the whole
    dependency graph inside the container. With uv.lock bind-mounted from the
    worktree, that resolution was written straight back over the contributor's
    lock - thousands of lines, no warning, and plausible enough to be mistaken
    for their own change.
    
    Generated-by: Claude Opus 5
---
 .../src/airflow_breeze/params/shell_params.py      | 11 +++++++++
 .../airflow_breeze/utils/docker_command_utils.py   |  5 +++-
 dev/breeze/src/airflow_breeze/utils/path_utils.py  |  1 +
 dev/breeze/tests/test_shell_params.py              | 27 +++++++++++++++++++++-
 scripts/ci/docker-compose/local.yml                |  3 ---
 scripts/ci/docker-compose/mount-uv-lock.yml        | 27 ++++++++++++++++++++++
 6 files changed, 69 insertions(+), 5 deletions(-)

diff --git a/dev/breeze/src/airflow_breeze/params/shell_params.py 
b/dev/breeze/src/airflow_breeze/params/shell_params.py
index 9bcac54db23..af5c044417a 100644
--- a/dev/breeze/src/airflow_breeze/params/shell_params.py
+++ b/dev/breeze/src/airflow_breeze/params/shell_params.py
@@ -101,6 +101,7 @@ from airflow_breeze.utils.path_utils import (
     SCRIPTS_CI_DOCKER_COMPOSE_LOCAL_ALL_SOURCES_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_LOCAL_YAML_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UI_DIST_PATH,
+    SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UV_LOCK_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_MYPY_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_PROVIDERS_AND_TESTS_SOURCES_PATH,
@@ -419,8 +420,18 @@ class ShellParams:
             
compose_file_list.append(SCRIPTS_CI_DOCKER_COMPOSE_DEBUG_PORTS_PATH)
         if self.mount_sources == MOUNT_SELECTED:
             compose_file_list.append(SCRIPTS_CI_DOCKER_COMPOSE_LOCAL_YAML_PATH)
+            if not self.force_lowest_dependencies:
+                
compose_file_list.append(SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UV_LOCK_PATH)
         elif self.mount_sources == MOUNT_ALL:
             
compose_file_list.append(SCRIPTS_CI_DOCKER_COMPOSE_LOCAL_ALL_SOURCES_PATH)
+            if self.force_lowest_dependencies:
+                # The whole worktree is bound here, so uv.lock cannot be left 
out of the mount.
+                console_print(
+                    "\n[warning]--force-lowest-dependencies with 
--mount-sources all will rewrite "
+                    "your uv.lock: the lowest-direct `uv sync` writes its 
re-resolved lock through "
+                    "the mounted worktree. Use the default --mount-sources 
selected to keep it "
+                    "untouched, or restore it afterwards with `git checkout -- 
uv.lock`.[/]\n"
+                )
         elif self.mount_sources == MOUNT_TESTS:
             
compose_file_list.append(SCRIPTS_CI_DOCKER_COMPOSE_TESTS_SOURCES_PATH)
         elif self.mount_sources == MOUNT_PROVIDERS_AND_TESTS:
diff --git a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py 
b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py
index da6a9e32626..21604a24145 100644
--- a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py
+++ b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py
@@ -114,13 +114,16 @@ VOLUMES_FOR_SELECTED_MOUNTS = [
     ("registry", "/opt/airflow/registry"),
     ("pyproject.toml", "/opt/airflow/pyproject.toml"),
     ("scripts", "/opt/airflow/scripts"),
-    ("uv.lock", "/opt/airflow/uv.lock"),
     ("scripts/docker/entrypoint_ci.sh", "/entrypoint"),
     ("shared", "/opt/airflow/shared"),
     ("task-sdk", "/opt/airflow/task-sdk"),
     ("ts-sdk", "/opt/airflow/ts-sdk"),
 ]
 
+# ``uv.lock`` is deliberately absent above: it is mounted from 
``mount-uv-lock.yml``, which
+# ShellParams skips for ``--force-lowest-dependencies`` so that the 
lowest-direct ``uv sync``
+# run in the container cannot write its re-resolved lock back over the host's.
+
 DOCKER_INFO_TIMEOUT = 30
 
 
diff --git a/dev/breeze/src/airflow_breeze/utils/path_utils.py 
b/dev/breeze/src/airflow_breeze/utils/path_utils.py
index 414fca44116..49bea72395e 100644
--- a/dev/breeze/src/airflow_breeze/utils/path_utils.py
+++ b/dev/breeze/src/airflow_breeze/utils/path_utils.py
@@ -523,6 +523,7 @@ SCRIPTS_CI_DOCKER_COMPOSE_INTEGRATION_KERBEROS_PATH = (
 SCRIPTS_CI_DOCKER_COMPOSE_LOCAL_ALL_SOURCES_PATH = 
SCRIPTS_CI_DOCKER_COMPOSE_PATH / "local-all-sources.yml"
 SCRIPTS_CI_DOCKER_COMPOSE_LOCAL_YAML_PATH = SCRIPTS_CI_DOCKER_COMPOSE_PATH / 
"local.yml"
 SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UI_DIST_PATH = SCRIPTS_CI_DOCKER_COMPOSE_PATH 
/ "mount-ui-dist.yml"
+SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UV_LOCK_PATH = SCRIPTS_CI_DOCKER_COMPOSE_PATH 
/ "mount-uv-lock.yml"
 SCRIPTS_CI_DOCKER_COMPOSE_MYPY_PATH = SCRIPTS_CI_DOCKER_COMPOSE_PATH / 
"mypy.yml"
 SCRIPTS_CI_DOCKER_COMPOSE_PYCACHE_PATH = SCRIPTS_CI_DOCKER_COMPOSE_PATH / 
"pycache.yml"
 SCRIPTS_CI_DOCKER_COMPOSE_PROVIDERS_AND_TESTS_SOURCES_PATH = (
diff --git a/dev/breeze/tests/test_shell_params.py 
b/dev/breeze/tests/test_shell_params.py
index 67980076b5c..cf12d38d19e 100644
--- a/dev/breeze/tests/test_shell_params.py
+++ b/dev/breeze/tests/test_shell_params.py
@@ -25,10 +25,12 @@ import yaml
 from rich.console import Console
 
 from airflow_breeze.branch_defaults import AIRFLOW_BRANCH
-from airflow_breeze.global_constants import PYCACHE_PREFIX_IN_CONTAINER
+from airflow_breeze.global_constants import MOUNT_SELECTED, 
PYCACHE_PREFIX_IN_CONTAINER
 from airflow_breeze.params.shell_params import ShellParams
 from airflow_breeze.utils.path_utils import (
     SCRIPTS_CI_DOCKER_COMPOSE_BASE_PATH,
+    SCRIPTS_CI_DOCKER_COMPOSE_LOCAL_YAML_PATH,
+    SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UV_LOCK_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_PYCACHE_PATH,
 )
@@ -285,3 +287,26 @@ def 
test_pycache_volume_compose_file_is_included_only_when_requested(
 def test_include_mypy_volume_adds_mypy_compose_file():
     compose_files = 
ShellParams(include_mypy_volume=True).compose_file.split(":")
     assert str(SCRIPTS_CI_DOCKER_COMPOSE_PATH / "mypy.yml") in compose_files
+
+
[email protected](("force_lowest_dependencies", "expected_count"), 
[(True, 0), (False, 1)])
+def 
test_uv_lock_is_not_mounted_for_lowest_dependencies(force_lowest_dependencies: 
bool, expected_count: int):
+    """The lowest-direct ``uv sync`` rewrites uv.lock, so its mount is dropped 
for that run."""
+    compose_files = ShellParams(
+        mount_sources=MOUNT_SELECTED, 
force_lowest_dependencies=force_lowest_dependencies
+    ).compose_file.split(os.pathsep)
+    assert 
compose_files.count(str(SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UV_LOCK_PATH)) == 
expected_count
+
+
+def test_uv_lock_is_mounted_only_by_its_own_compose_file():
+    """Guards the split: a stray uv.lock bind in local.yml would defeat the 
skip above."""
+    local_compose_file = 
yaml.safe_load(SCRIPTS_CI_DOCKER_COMPOSE_LOCAL_YAML_PATH.read_text())
+    # local.yml mixes named volumes (plain "source:target" strings) with bind 
mappings.
+    local_volumes = local_compose_file["services"]["airflow"]["volumes"]
+    targets = [volume["target"] if isinstance(volume, dict) else volume for 
volume in local_volumes]
+    assert not any("uv.lock" in target for target in targets)
+
+    uv_lock_compose_file = 
yaml.safe_load(SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UV_LOCK_PATH.read_text())
+    assert uv_lock_compose_file["services"]["airflow"]["volumes"] == [
+        {"type": "bind", "source": "../../../uv.lock", "target": 
"/opt/airflow/uv.lock"}
+    ]
diff --git a/scripts/ci/docker-compose/local.yml 
b/scripts/ci/docker-compose/local.yml
index 4f389106f6e..54b19a2affa 100644
--- a/scripts/ci/docker-compose/local.yml
+++ b/scripts/ci/docker-compose/local.yml
@@ -120,9 +120,6 @@ services:
       - type: bind
         source: ../../../scripts
         target: /opt/airflow/scripts
-      - type: bind
-        source: ../../../uv.lock
-        target: /opt/airflow/uv.lock
       - type: bind
         source: ../../../scripts/docker/entrypoint_ci.sh
         target: /entrypoint
diff --git a/scripts/ci/docker-compose/mount-uv-lock.yml 
b/scripts/ci/docker-compose/mount-uv-lock.yml
new file mode 100644
index 00000000000..35ac1c7ea0c
--- /dev/null
+++ b/scripts/ci/docker-compose/mount-uv-lock.yml
@@ -0,0 +1,27 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+---
+services:
+  airflow:
+    # Kept out of local.yml so that it can be skipped: `uv sync` rewrites 
uv.lock in place, and
+    # with --force-lowest-dependencies it re-resolves the whole graph, so 
mounting the host file
+    # would silently replace the contributor's lock with a lowest-direct 
resolution. Without the
+    # mount the container falls back to the uv.lock baked into the CI image 
and writes there.
+    volumes:
+      - type: bind
+        source: ../../../uv.lock
+        target: /opt/airflow/uv.lock

Reply via email to