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"]