From 0acb41590bb3d840462f764abfbe4269fc0e7746 Mon Sep 17 00:00:00 2001 From: Shahar Epstein <60007259+shahar1@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:40:19 +0200 Subject: [PATCH] Stop breeze k8s commands from silently rewriting uv.lock A v3-4-test backport bumped a workspace member's version without updating uv.lock, and the `update-uv-lock` static check still passed. The chart's prek hooks run `breeze k8s setup-env`, which ran a plain `uv sync` in kubernetes-tests. That project is a uv workspace member, so the sync re-resolved and rewrote the root uv.lock before the root hook checked it. Nested prek configs do not inherit the root UV_FROZEN=1, and prek does not report files a nested-project hook modifies outside its own project, so the rewrite went unnoticed and the stale lock was merged. It then broke every "Generate constraints" job on the branch. The `uv run pytest` calls of `breeze k8s tests` and the overlay tests had the same effect on contributors' checkouts. CI now also fails when the static checks leave tracked files modified, so any other hook writing outside its project is caught. --- .github/workflows/basic-tests.yml | 11 ++++++++++ .github/workflows/ci-image-checks.yml | 11 ++++++++++ .../commands/kubernetes_commands.py | 2 +- .../commands/kubernetes_kustomize_commands.py | 2 +- .../airflow_breeze/utils/kubernetes_utils.py | 3 ++- dev/breeze/tests/test_kubernetes_commands.py | 17 ++++++++++++++++ .../test_kubernetes_kustomize_commands.py | 20 +++++++++++++++++++ dev/breeze/tests/test_kubernetes_utils.py | 7 +++++++ 8 files changed, 70 insertions(+), 3 deletions(-) diff --git a/.github/workflows/basic-tests.yml b/.github/workflows/basic-tests.yml index 944315d8e8350..9d3a9046e5695 100644 --- a/.github/workflows/basic-tests.yml +++ b/.github/workflows/basic-tests.yml @@ -332,6 +332,17 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SKIP: ${{ inputs.skip-prek-hooks }} COLUMNS: "202" + - name: "Fail if static checks modified tracked files" + # prek does not report files that a nested-project hook modifies outside its own project, + # so check the tree independently. + shell: bash + run: | + if ! git diff --quiet; then + echo "::error::Static checks passed but modified tracked files. Commit the changes below." + git diff --stat + git diff | head -n 500 || true + exit 1 + fi test-git-clone-on-windows: timeout-minutes: 5 diff --git a/.github/workflows/ci-image-checks.yml b/.github/workflows/ci-image-checks.yml index cca6c1826b47c..ce2e8c41b33f5 100644 --- a/.github/workflows/ci-image-checks.yml +++ b/.github/workflows/ci-image-checks.yml @@ -162,6 +162,17 @@ jobs: PR_NUMBER: ${{ inputs.newsfragment-pr-number }} DEFAULT_BRANCH: ${{ inputs.branch }} RUFF_FORMAT: "github" + - name: "Fail if static checks modified tracked files" + # prek does not report files that a nested-project hook modifies outside its own project + # (e.g. `breeze k8s setup-env` relocking uv.lock), so check the tree independently. + shell: bash + run: | + if ! git diff --quiet; then + echo "::error::Static checks passed but modified tracked files. Commit the changes below." + git diff --stat + git diff | head -n 500 || true + exit 1 + fi - name: "Show prek log on failure" run: cat ~/.cache/prek/prek.log || true if: failure() diff --git a/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py b/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py index 4434c1422e629..e59f0aca69c9b 100644 --- a/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py @@ -2035,7 +2035,7 @@ def _run_tests( f"[info]You can deploy airflow with {executor} by running:[/]\nbreeze k8s configure-cluster\nbreeze k8s deploy-airflow --multi-namespace-mode --executor {executor}" ) return 1, f"Tests {kubectl_cluster_name}" - pytest_cmd = ["uv", "run", "pytest"] + pytest_cmd = ["uv", "run", "--frozen", "pytest"] the_tests: list[str] = ["tests"] ordered_unique_args = dict.fromkeys(chain(pytest_cmd, the_tests, test_args)) command_to_run = " ".join(quote(arg) for arg in ordered_unique_args) diff --git a/dev/breeze/src/airflow_breeze/commands/kubernetes_kustomize_commands.py b/dev/breeze/src/airflow_breeze/commands/kubernetes_kustomize_commands.py index d7aa3b2486229..fe1da3f0689ac 100644 --- a/dev/breeze/src/airflow_breeze/commands/kubernetes_kustomize_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/kubernetes_kustomize_commands.py @@ -488,7 +488,7 @@ def _run_overlay_pytest( env["OVERLAY_UNDER_TEST"] = overlay_name env["OVERLAY_NAMESPACE"] = namespace env["OVERLAY_RELEASE_NAME"] = release_name - pytest_cmd = ["uv", "run", "pytest", str(test_file.relative_to(CHART_PATH)), "-xvs"] + pytest_cmd = ["uv", "run", "--frozen", "pytest", str(test_file.relative_to(CHART_PATH)), "-xvs"] console_print(f"[info]Running behavioural tests: {' '.join(pytest_cmd)}") result = run_command(pytest_cmd, env=env, check=False, cwd=CHART_PATH.as_posix()) return result.returncode diff --git a/dev/breeze/src/airflow_breeze/utils/kubernetes_utils.py b/dev/breeze/src/airflow_breeze/utils/kubernetes_utils.py index d6e9c343b9b1b..55b1a9195bd4e 100644 --- a/dev/breeze/src/airflow_breeze/utils/kubernetes_utils.py +++ b/dev/breeze/src/airflow_breeze/utils/kubernetes_utils.py @@ -291,7 +291,8 @@ def make_sure_kubernetes_tools_are_installed(): def sync_virtualenv(force_venv_setup: bool) -> RunCommandResult: - uv_sync_cmd = ["uv", "sync"] + # kubernetes-tests is a workspace member: a plain `uv sync` would re-resolve and rewrite the root uv.lock. + uv_sync_cmd = ["uv", "sync", "--frozen"] if force_venv_setup: uv_sync_cmd.append("--reinstall") if os.environ.get("CI", "false") == "true": diff --git a/dev/breeze/tests/test_kubernetes_commands.py b/dev/breeze/tests/test_kubernetes_commands.py index 142e389bf41a8..a40e15660b866 100644 --- a/dev/breeze/tests/test_kubernetes_commands.py +++ b/dev/breeze/tests/test_kubernetes_commands.py @@ -124,3 +124,20 @@ def test_commands_pass_the_github_repository_from_the_environment( assert result.exit_code == 0, result.output mock_helper.assert_called_once() assert mock_helper.call_args.kwargs["github_repository"] == FORK_REPOSITORY + + +@mock.patch.object(kubernetes_commands, "run_command", autospec=True) +@mock.patch.object(kubernetes_commands, "get_kubectl_cluster_name", autospec=True, return_value="airflow") +@mock.patch.object(kubernetes_commands, "get_k8s_env", autospec=True, return_value={"SHELL": "/bin/bash"}) +def test_run_tests_does_not_relock(mock_get_k8s_env, mock_get_cluster_name, mock_run_command): + mock_run_command.return_value = mock.Mock(returncode=0) + + kubernetes_commands._run_tests( + python="3.11", + kubernetes_version="v1.31.12", + output=None, + executor="LocalExecutor", + test_args=[], + ) + + assert mock_run_command.call_args.args[0][-1].startswith("uv run --frozen pytest ") diff --git a/dev/breeze/tests/test_kubernetes_kustomize_commands.py b/dev/breeze/tests/test_kubernetes_kustomize_commands.py index 0c6d48deea3a7..e58ac56b1fb27 100644 --- a/dev/breeze/tests/test_kubernetes_kustomize_commands.py +++ b/dev/breeze/tests/test_kubernetes_kustomize_commands.py @@ -16,8 +16,11 @@ # under the License. from __future__ import annotations +from unittest import mock + import pytest +from airflow_breeze.commands import kubernetes_kustomize_commands from airflow_breeze.commands.kubernetes_kustomize_commands import ( ALLOWED_OVERLAY_IMAGES, _discover_overlay_images, @@ -76,3 +79,20 @@ def test_allowed_images_yield_no_findings(self): ) def test_flags_images_not_on_allow_list(self, images, expected): assert _find_disallowed_overlay_images(images) == expected + + +@mock.patch.object(kubernetes_kustomize_commands, "run_command", autospec=True) +@mock.patch.object(kubernetes_kustomize_commands, "get_k8s_env", autospec=True, return_value={}) +def test_run_overlay_pytest_does_not_relock(mock_get_k8s_env, mock_run_command): + mock_run_command.return_value = mock.Mock(returncode=0) + + kubernetes_kustomize_commands._run_overlay_pytest( + overlay_name="kerberos", + namespace="airflow", + release_name="airflow", + python="3.11", + kubernetes_version="v1.31.12", + executor="LocalExecutor", + ) + + assert mock_run_command.call_args.args[0][:4] == ["uv", "run", "--frozen", "pytest"] diff --git a/dev/breeze/tests/test_kubernetes_utils.py b/dev/breeze/tests/test_kubernetes_utils.py index 88dee2d715fa0..d0ce39f84d212 100644 --- a/dev/breeze/tests/test_kubernetes_utils.py +++ b/dev/breeze/tests/test_kubernetes_utils.py @@ -27,6 +27,7 @@ SCRIPTS_CI_KUBERNETES_PATH, _render_kind_cluster_config, get_kubernetes_port_numbers, + sync_virtualenv, ) # Rendered config of a cluster created before the switch to single-node clusters — @@ -84,3 +85,9 @@ def test_get_kubernetes_port_numbers_raises_when_forwarded_port_mapping_is_missi def test_nodeport_service_exposes_forwarded_node_port(): node_port_service = yaml.safe_load((SCRIPTS_CI_KUBERNETES_PATH / "nodeport.yaml").read_text()) assert [port["nodePort"] for port in node_port_service["spec"]["ports"]] == [FORWARDED_NODE_PORT] + + +@patch("airflow_breeze.utils.kubernetes_utils.run_command", autospec=True) +def test_sync_virtualenv_does_not_relock(mock_run_command): + sync_virtualenv(force_venv_setup=False) + assert mock_run_command.call_args.args[0][:3] == ["uv", "sync", "--frozen"]