[ci] Run downstream device-builder tests against PR Python code

Add a CI job that checks out esphome/device-builder@main, installs it,
overlays the PR's esphome via `pip install -e ./esphome`, and runs its
pytest suite. Gated by determine-jobs.py: only runs when Python files
under esphome/ change or runtime deps (requirements.txt, pyproject.toml)
change -- the surface device-builder consumes when it imports esphome.

This catches downstream breakage at PR time instead of after release,
mirroring the yarl -> aiohttp pattern.
This commit is contained in:
J. Nick Koston
2026-05-03 08:49:59 -05:00
parent 8046ff7e1e
commit 365d93f01b
3 changed files with 139 additions and 0 deletions
+37
View File
@@ -10,6 +10,7 @@ what files have changed. It outputs JSON with the following structure:
"clang_tidy": true/false,
"clang_format": true/false,
"python_linters": true/false,
"device_builder": true/false,
"changed_components": ["component1", "component2", ...],
"component_test_count": 5,
"memory_impact": {
@@ -25,6 +26,7 @@ The CI workflow uses this information to:
- Skip or run clang-tidy (and whether to do a full scan)
- Skip or run clang-format
- Skip or run Python linters (ruff, flake8, pylint, pyupgrade)
- Skip or run downstream esphome/device-builder tests against the PR's Python code
- Determine which components to test individually
- Decide how to split component tests (if there are many)
- Run memory impact analysis whenever there are changed components (merged config), and also for core-only changes
@@ -440,6 +442,39 @@ def should_run_import_time(branch: str | None = None) -> bool:
return False
# Files outside esphome/**/*.py whose changes can affect the downstream
# device-builder build. requirements.txt / pyproject.toml change the runtime
# dependency graph that device-builder picks up when it installs esphome.
DEVICE_BUILDER_TRIGGER_FILES = frozenset(
{
"requirements.txt",
"pyproject.toml",
}
)
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.
Args:
branch: Branch to compare against. If None, uses default.
Returns:
True if the device-builder downstream tests should run, False otherwise.
"""
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
return False
def determine_cpp_unit_tests(
branch: str | None = None,
) -> tuple[bool, list[str]]:
@@ -874,6 +909,7 @@ def main() -> None:
run_clang_format = should_run_clang_format(args.branch)
run_python_linters = should_run_python_linters(args.branch)
run_import_time = should_run_import_time(args.branch)
run_device_builder = should_run_device_builder(args.branch)
changed_cpp_file_count = count_changed_cpp_files(args.branch)
# Get changed components
@@ -1007,6 +1043,7 @@ def main() -> None:
"clang_format": run_clang_format,
"python_linters": run_python_linters,
"import_time": run_import_time,
"device_builder": run_device_builder,
"changed_components": changed_components,
"changed_components_with_tests": changed_components_with_tests,
"directly_changed_components_with_tests": list(directly_changed_with_tests),