diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0363b5afdf..7a731a1b02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -179,6 +179,7 @@ jobs: changed-components: ${{ steps.determine.outputs.changed-components }} changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }} component-test-count: ${{ steps.determine.outputs.component-test-count }} + memory_impact: ${{ steps.determine.outputs.memory-impact }} steps: - name: Check out code from GitHub uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -207,6 +208,7 @@ jobs: echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT + echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT integration-tests: name: Run integration tests @@ -510,6 +512,118 @@ jobs: - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 if: always() + memory-impact-target-branch: + name: Build target branch for memory impact + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' + outputs: + ram_usage: ${{ steps.extract.outputs.ram_usage }} + flash_usage: ${{ steps.extract.outputs.flash_usage }} + steps: + - name: Check out target branch + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + ref: ${{ github.base_ref }} + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Cache platformio + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.platformio + key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} + - name: Compile test configuration and extract memory usage + id: extract + run: | + . venv/bin/activate + component="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).component }}" + platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" + test_file="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).test_file }}" + + echo "Compiling $component for $platform using $test_file" + python script/test_build_components.py -e compile -c "$component" -t "$platform" --no-grouping 2>&1 | \ + python script/ci_memory_impact_extract.py --output-env + + memory-impact-pr-branch: + name: Build PR branch for memory impact + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' + outputs: + ram_usage: ${{ steps.extract.outputs.ram_usage }} + flash_usage: ${{ steps.extract.outputs.flash_usage }} + steps: + - name: Check out PR branch + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Cache platformio + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.platformio + key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} + - name: Compile test configuration and extract memory usage + id: extract + run: | + . venv/bin/activate + component="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).component }}" + platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" + test_file="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).test_file }}" + + echo "Compiling $component for $platform using $test_file" + python script/test_build_components.py -e compile -c "$component" -t "$platform" --no-grouping 2>&1 | \ + python script/ci_memory_impact_extract.py --output-env + + memory-impact-comment: + name: Comment memory impact + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + - memory-impact-target-branch + - memory-impact-pr-branch + if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' + permissions: + contents: read + pull-requests: write + steps: + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Post or update PR comment + env: + GH_TOKEN: ${{ github.token }} + COMPONENT: ${{ fromJSON(needs.determine-jobs.outputs.memory_impact).component }} + PLATFORM: ${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }} + TARGET_RAM: ${{ needs.memory-impact-target-branch.outputs.ram_usage }} + TARGET_FLASH: ${{ needs.memory-impact-target-branch.outputs.flash_usage }} + PR_RAM: ${{ needs.memory-impact-pr-branch.outputs.ram_usage }} + PR_FLASH: ${{ needs.memory-impact-pr-branch.outputs.flash_usage }} + run: | + . venv/bin/activate + python script/ci_memory_impact_comment.py \ + --pr-number "${{ github.event.pull_request.number }}" \ + --component "$COMPONENT" \ + --platform "$PLATFORM" \ + --target-ram "$TARGET_RAM" \ + --target-flash "$TARGET_FLASH" \ + --pr-ram "$PR_RAM" \ + --pr-flash "$PR_FLASH" + ci-status: name: CI Status runs-on: ubuntu-24.04 @@ -525,6 +639,9 @@ jobs: - test-build-components-splitter - test-build-components-split - pre-commit-ci-lite + - memory-impact-target-branch + - memory-impact-pr-branch + - memory-impact-comment if: always() steps: - name: Success diff --git a/.github/workflows/memory-impact.yml b/.github/workflows/memory-impact.yml deleted file mode 100644 index dff73e6cd7..0000000000 --- a/.github/workflows/memory-impact.yml +++ /dev/null @@ -1,150 +0,0 @@ ---- -name: Memory Impact Analysis - -on: - pull_request: - paths: - - "esphome/components/**" - - "esphome/core/**" - -permissions: - contents: read - pull-requests: write - -env: - DEFAULT_PYTHON: "3.11" - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - detect-single-component: - name: Detect single component change - runs-on: ubuntu-24.04 - outputs: - should_run: ${{ steps.detect.outputs.should_run }} - component: ${{ steps.detect.outputs.component }} - test_file: ${{ steps.detect.outputs.test_file }} - platform: ${{ steps.detect.outputs.platform }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - fetch-depth: 0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install PyYAML - - name: Detect single component change - id: detect - run: | - python script/ci_memory_impact_detector.py - - build-target-branch: - name: Build target branch - runs-on: ubuntu-24.04 - needs: detect-single-component - if: needs.detect-single-component.outputs.should_run == 'true' - outputs: - ram_usage: ${{ steps.extract.outputs.ram_usage }} - flash_usage: ${{ steps.extract.outputs.flash_usage }} - steps: - - name: Check out target branch - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - ref: ${{ github.base_ref }} - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Install ESPHome - run: | - pip install -e . - - name: Cache platformio - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: ~/.platformio - key: platformio-memory-${{ needs.detect-single-component.outputs.platform }}-${{ hashFiles('platformio.ini') }} - - name: Compile test configuration and extract memory usage - id: extract - run: | - component="${{ needs.detect-single-component.outputs.component }}" - platform="${{ needs.detect-single-component.outputs.platform }}" - test_file="${{ needs.detect-single-component.outputs.test_file }}" - - echo "Compiling $component for $platform using $test_file" - python script/test_build_components.py -e compile -c "$component" -t "$platform" --no-grouping 2>&1 | \ - python script/ci_memory_impact_extract.py --output-env - - build-pr-branch: - name: Build PR branch - runs-on: ubuntu-24.04 - needs: detect-single-component - if: needs.detect-single-component.outputs.should_run == 'true' - outputs: - ram_usage: ${{ steps.extract.outputs.ram_usage }} - flash_usage: ${{ steps.extract.outputs.flash_usage }} - steps: - - name: Check out PR branch - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Install ESPHome - run: | - pip install -e . - - name: Cache platformio - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: ~/.platformio - key: platformio-memory-${{ needs.detect-single-component.outputs.platform }}-${{ hashFiles('platformio.ini') }} - - name: Compile test configuration and extract memory usage - id: extract - run: | - component="${{ needs.detect-single-component.outputs.component }}" - platform="${{ needs.detect-single-component.outputs.platform }}" - test_file="${{ needs.detect-single-component.outputs.test_file }}" - - echo "Compiling $component for $platform using $test_file" - python script/test_build_components.py -e compile -c "$component" -t "$platform" --no-grouping 2>&1 | \ - python script/ci_memory_impact_extract.py --output-env - - comment-results: - name: Comment memory impact - runs-on: ubuntu-24.04 - needs: - - detect-single-component - - build-target-branch - - build-pr-branch - if: needs.detect-single-component.outputs.should_run == 'true' - steps: - - name: Check out code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Post or update PR comment - env: - GH_TOKEN: ${{ github.token }} - COMPONENT: ${{ needs.detect-single-component.outputs.component }} - PLATFORM: ${{ needs.detect-single-component.outputs.platform }} - TARGET_RAM: ${{ needs.build-target-branch.outputs.ram_usage }} - TARGET_FLASH: ${{ needs.build-target-branch.outputs.flash_usage }} - PR_RAM: ${{ needs.build-pr-branch.outputs.ram_usage }} - PR_FLASH: ${{ needs.build-pr-branch.outputs.flash_usage }} - run: | - python script/ci_memory_impact_comment.py \ - --pr-number "${{ github.event.pull_request.number }}" \ - --component "$COMPONENT" \ - --platform "$PLATFORM" \ - --target-ram "$TARGET_RAM" \ - --target-flash "$TARGET_FLASH" \ - --pr-ram "$PR_RAM" \ - --pr-flash "$PR_FLASH" diff --git a/script/ci_memory_impact_detector.py b/script/ci_memory_impact_detector.py deleted file mode 100755 index 8c3045ab00..0000000000 --- a/script/ci_memory_impact_detector.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env python3 -"""Detect if a PR changes exactly one component for memory impact analysis. - -This script is used by the CI workflow to determine if a PR should trigger -memory impact analysis. The analysis only runs when: -1. Exactly one component has changed (not counting core changes) -2. The component has at least one test configuration - -The script outputs GitHub Actions environment variables to control the workflow. -""" - -from __future__ import annotations - -from pathlib import Path -import sys - -# Add esphome to path -sys.path.insert(0, str(Path(__file__).parent.parent)) - -# pylint: disable=wrong-import-position -from script.ci_helpers import write_github_output -from script.helpers import ESPHOME_COMPONENTS_PATH, changed_files - -# Platform preference order for memory impact analysis -# Ordered by production relevance and memory constraint importance -PLATFORM_PREFERENCE = [ - "esp32-idf", # Primary ESP32 IDF platform - "esp32-c3-idf", # ESP32-C3 IDF - "esp32-c6-idf", # ESP32-C6 IDF - "esp32-s2-idf", # ESP32-S2 IDF - "esp32-s3-idf", # ESP32-S3 IDF - "esp32-c2-idf", # ESP32-C2 IDF - "esp32-c5-idf", # ESP32-C5 IDF - "esp32-h2-idf", # ESP32-H2 IDF - "esp32-p4-idf", # ESP32-P4 IDF - "esp8266-ard", # ESP8266 Arduino (memory constrained) - "esp32-ard", # ESP32 Arduino - "esp32-c3-ard", # ESP32-C3 Arduino - "esp32-s2-ard", # ESP32-S2 Arduino - "esp32-s3-ard", # ESP32-S3 Arduino - "bk72xx-ard", # BK72xx Arduino - "rp2040-ard", # RP2040 Arduino - "nrf52-adafruit", # nRF52 Adafruit - "host", # Host platform (development/testing) -] - - -def find_test_for_component(component: str) -> tuple[str | None, str | None]: - """Find a test configuration for the given component. - - Prefers platforms based on PLATFORM_PREFERENCE order. - - Args: - component: Component name - - Returns: - Tuple of (test_file_name, platform) or (None, None) if no test found - """ - tests_dir = Path(__file__).parent.parent / "tests" / "components" / component - - if not tests_dir.exists(): - return None, None - - # Look for test files - test_files = list(tests_dir.glob("test.*.yaml")) - if not test_files: - return None, None - - # Try each preferred platform in order - for preferred_platform in PLATFORM_PREFERENCE: - for test_file in test_files: - parts = test_file.stem.split(".") - if len(parts) >= 2: - platform = parts[1] - if platform == preferred_platform: - return test_file.name, platform - - # Fall back to first test file - test_file = test_files[0] - parts = test_file.stem.split(".") - platform = parts[1] if len(parts) >= 2 else "esp32-idf" - return test_file.name, platform - - -def detect_single_component_change() -> None: - """Detect if exactly one component changed and output GitHub Actions variables.""" - files = changed_files() - - # Find all changed components (excluding core) - changed_components = set() - - for file in files: - if file.startswith(ESPHOME_COMPONENTS_PATH): - parts = file.split("/") - if len(parts) >= 3: - component = parts[2] - # Skip base bus components as they're used across many builds - if component not in ["i2c", "spi", "uart", "modbus"]: - changed_components.add(component) - - # Only proceed if exactly one component changed - if len(changed_components) != 1: - print( - f"Found {len(changed_components)} component(s) changed, skipping memory analysis" - ) - write_github_output({"should_run": "false"}) - return - - component = list(changed_components)[0] - print(f"Detected single component change: {component}") - - # Find a test configuration for this component - test_file, platform = find_test_for_component(component) - - if not test_file: - print(f"No test configuration found for {component}, skipping memory analysis") - write_github_output({"should_run": "false"}) - return - - print(f"Found test: {test_file} for platform: {platform}") - print("Memory impact analysis will run") - - write_github_output( - { - "should_run": "true", - "component": component, - "test_file": test_file, - "platform": platform, - } - ) - - -if __name__ == "__main__": - detect_single_component_change() diff --git a/script/determine-jobs.py b/script/determine-jobs.py index a078fd8f9b..78fd32c3f4 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -10,7 +10,13 @@ what files have changed. It outputs JSON with the following structure: "clang_format": true/false, "python_linters": true/false, "changed_components": ["component1", "component2", ...], - "component_test_count": 5 + "component_test_count": 5, + "memory_impact": { + "should_run": "true/false", + "component": "component_name", + "test_file": "test.esp32-idf.yaml", + "platform": "esp32-idf" + } } The CI workflow uses this information to: @@ -20,6 +26,7 @@ The CI workflow uses this information to: - Skip or run Python linters (ruff, flake8, pylint, pyupgrade) - Determine which components to test individually - Decide how to split component tests (if there are many) +- Run memory impact analysis when exactly one component changes Usage: python script/determine-jobs.py [-b BRANCH] @@ -212,6 +219,92 @@ def _any_changed_file_endswith(branch: str | None, extensions: tuple[str, ...]) return any(file.endswith(extensions) for file in changed_files(branch)) +def detect_single_component_for_memory_impact( + changed_components: list[str], +) -> dict[str, Any]: + """Detect if exactly one component changed for memory impact analysis. + + Args: + changed_components: List of changed component names + + Returns: + Dictionary with memory impact analysis parameters: + - should_run: "true" or "false" + - component: component name (if should_run is true) + - test_file: test file name (if should_run is true) + - platform: platform name (if should_run is true) + """ + # Platform preference order for memory impact analysis + # Ordered by production relevance and memory constraint importance + PLATFORM_PREFERENCE = [ + "esp32-idf", # Primary ESP32 IDF platform + "esp32-c3-idf", # ESP32-C3 IDF + "esp32-c6-idf", # ESP32-C6 IDF + "esp32-s2-idf", # ESP32-S2 IDF + "esp32-s3-idf", # ESP32-S3 IDF + "esp32-c2-idf", # ESP32-C2 IDF + "esp32-c5-idf", # ESP32-C5 IDF + "esp32-h2-idf", # ESP32-H2 IDF + "esp32-p4-idf", # ESP32-P4 IDF + "esp8266-ard", # ESP8266 Arduino (memory constrained) + "esp32-ard", # ESP32 Arduino + "esp32-c3-ard", # ESP32-C3 Arduino + "esp32-s2-ard", # ESP32-S2 Arduino + "esp32-s3-ard", # ESP32-S3 Arduino + "bk72xx-ard", # BK72xx Arduino + "rp2040-ard", # RP2040 Arduino + "nrf52-adafruit", # nRF52 Adafruit + "host", # Host platform (development/testing) + ] + + # Skip base bus components as they're used across many builds + filtered_components = [ + c for c in changed_components if c not in ["i2c", "spi", "uart", "modbus"] + ] + + # Only proceed if exactly one component changed + if len(filtered_components) != 1: + return {"should_run": "false"} + + component = filtered_components[0] + + # Find a test configuration for this component + tests_dir = Path(root_path) / "tests" / "components" / component + + if not tests_dir.exists(): + return {"should_run": "false"} + + # Look for test files + test_files = list(tests_dir.glob("test.*.yaml")) + if not test_files: + return {"should_run": "false"} + + # Try each preferred platform in order + for preferred_platform in PLATFORM_PREFERENCE: + for test_file in test_files: + parts = test_file.stem.split(".") + if len(parts) >= 2: + platform = parts[1] + if platform == preferred_platform: + return { + "should_run": "true", + "component": component, + "test_file": test_file.name, + "platform": platform, + } + + # Fall back to first test file + test_file = test_files[0] + parts = test_file.stem.split(".") + platform = parts[1] if len(parts) >= 2 else "esp32-idf" + return { + "should_run": "true", + "component": component, + "test_file": test_file.name, + "platform": platform, + } + + def main() -> None: """Main function that determines which CI jobs to run.""" parser = argparse.ArgumentParser( @@ -247,6 +340,9 @@ def main() -> None: and any(component_test_dir.glob("test.*.yaml")) ] + # Detect single component change for memory impact analysis + memory_impact = detect_single_component_for_memory_impact(changed_components) + # Build output output: dict[str, Any] = { "integration_tests": run_integration, @@ -256,6 +352,7 @@ def main() -> None: "changed_components": changed_components, "changed_components_with_tests": changed_components_with_tests, "component_test_count": len(changed_components_with_tests), + "memory_impact": memory_impact, } # Output as JSON