[ci] Address Copilot review on device-builder gate

- Skip the device-builder downstream job on beta/release target
  branches. Those branches lag behind device-builder@main, so a
  newer device-builder API requirement would falsely fail the run
  without reflecting any problem in the PR itself. Mirrors the
  same skip detect_memory_impact_config already does.
- Broaden the trigger to any non-C++ file under esphome/. The
  package ships data files via include-package-data = true (e.g.
  esphome/idf_component.yml, dashboard templates, JSON), so a
  Python-only filter under-fires for changes that still affect
  what device-builder installs.

Tests cover both: per-file behavior (with the skip mocked off) and
the beta/release skip itself short-circuiting before changed_files
is even consulted.
This commit is contained in:
J. Nick Koston
2026-05-03 09:12:04 -05:00
parent ed00f5f36b
commit 520371c4a2
2 changed files with 59 additions and 10 deletions
+22 -5
View File
@@ -457,9 +457,15 @@ def should_run_device_builder(branch: str | None = None) -> bool:
"""Determine if downstream esphome/device-builder tests should run.
device-builder imports esphome as a library, so whenever the importable
Python surface or the runtime dependencies change we re-run its test
suite against the PR's code to catch breakage we'd otherwise only see
after a release.
Python surface, the runtime dependencies, or any non-C++ file packaged
with esphome (pyproject.toml has ``include-package-data = true``, so
things like esphome/idf_component.yml ship and can affect installs)
changes we re-run its test suite against the PR's code to catch
breakage we'd otherwise only see after a release.
Skipped on beta/release branches: those branches typically lag behind
device-builder@main, so a new device-builder API dependency would
falsely fail the run without reflecting any problem in the PR itself.
Args:
branch: Branch to compare against. If None, uses default.
@@ -467,11 +473,22 @@ def should_run_device_builder(branch: str | None = None) -> bool:
Returns:
True if the device-builder downstream tests should run, False otherwise.
"""
target_branch = get_target_branch()
if target_branch and (
target_branch.startswith("release") or target_branch.startswith("beta")
):
return False
for file in changed_files(branch):
if file.startswith("esphome/") and file.endswith(PYTHON_FILE_EXTENSIONS):
return True
if file in DEVICE_BUILDER_TRIGGER_FILES:
return True
# Anything under esphome/ that isn't C++ source can change the
# importable / packaged surface device-builder consumes
# (Python sources, packaged YAML/JSON like idf_component.yml,
# etc.). C++ files only affect compiled firmware, not the
# Python install device-builder pulls in.
if file.startswith("esphome/") and not file.endswith(CPP_FILE_EXTENSIONS):
return True
return False
+37 -5
View File
@@ -762,17 +762,27 @@ def test_should_run_import_time_with_branch() -> None:
# Runtime dependency changes trigger
(["requirements.txt"], True),
(["pyproject.toml"], True),
# Non-C++ files packaged with esphome trigger -- device-builder
# picks them up because esphome's pyproject sets
# include-package-data = true.
(["esphome/idf_component.yml"], True),
(["esphome/dashboard/templates/index.html"], True),
(["esphome/components/api/api_pb2_service.json"], True),
# Mixed: any triggering file is enough
(["docs/README.md", "esphome/config.py"], True),
# Dev/test-only dependency changes don't trigger device-builder
# (they don't affect the importable surface device-builder uses)
(["requirements_dev.txt"], False),
(["requirements_test.txt"], False),
# Python files outside esphome/ don't trigger
# Files outside esphome/ don't trigger
(["script/some_other_script.py"], False),
(["tests/script/test_determine_jobs.py"], False),
# Non-Python changes don't trigger
# C++ files under esphome/ don't trigger -- they only affect
# compiled firmware, not the Python install device-builder pulls in.
(["esphome/core/component.cpp"], False),
(["esphome/core/component.h"], False),
(["esphome/components/wifi/wifi_component.cpp"], False),
# Files outside esphome/ entirely
(["tests/components/wifi/test.esp32-idf.yaml"], False),
(["README.md"], False),
([], False),
@@ -781,20 +791,42 @@ def test_should_run_import_time_with_branch() -> None:
def test_should_run_device_builder(
changed_files: list[str], expected_result: bool
) -> None:
"""Test should_run_device_builder function."""
with patch.object(determine_jobs, "changed_files", return_value=changed_files):
"""Test should_run_device_builder function (non-beta/release target)."""
with (
patch.object(determine_jobs, "changed_files", return_value=changed_files),
# Mock target branch to "dev" so the beta/release skip is bypassed
# for these per-file behavior checks.
patch.object(determine_jobs, "get_target_branch", return_value="dev"),
):
result = determine_jobs.should_run_device_builder()
assert result == expected_result
def test_should_run_device_builder_with_branch() -> None:
"""Test should_run_device_builder with branch argument."""
with patch.object(determine_jobs, "changed_files") as mock_changed:
with (
patch.object(determine_jobs, "changed_files") as mock_changed,
patch.object(determine_jobs, "get_target_branch", return_value="dev"),
):
mock_changed.return_value = []
determine_jobs.should_run_device_builder("release")
mock_changed.assert_called_once_with("release")
@pytest.mark.parametrize("target_branch", ["beta", "release", "release-2026.5"])
def test_should_run_device_builder_skips_beta_release(target_branch: str) -> None:
"""Beta/release target branches skip device-builder (lag behind device-builder@main)."""
with (
patch.object(determine_jobs, "get_target_branch", return_value=target_branch),
patch.object(determine_jobs, "changed_files") as mock_changed,
):
# Even with a triggering file present, the target-branch guard wins.
mock_changed.return_value = ["esphome/__main__.py"]
assert determine_jobs.should_run_device_builder() is False
# changed_files shouldn't even be consulted -- the guard short-circuits.
mock_changed.assert_not_called()
@pytest.mark.parametrize(
("changed_files", "expected_result"),
[