Compare commits

...

2 Commits

Author SHA1 Message Date
J. Nick Koston
8622bf1de0 syntax error 2025-12-09 21:00:43 +01:00
J. Nick Koston
0420c00ec3 [ci] Allow memory impact target branch build to fail without blocking CI 2025-12-09 20:58:44 +01:00
4 changed files with 545 additions and 461 deletions

View File

@@ -959,13 +959,13 @@ jobs:
- memory-impact-comment - memory-impact-comment
if: always() if: always()
steps: steps:
- name: Success - name: Check job results
if: ${{ !(contains(needs.*.result, 'failure')) }}
run: exit 0
- name: Failure
if: ${{ contains(needs.*.result, 'failure') }}
env: env:
JSON_DOC: ${{ toJSON(needs) }} NEEDS_JSON: ${{ toJSON(needs) }}
run: | run: |
echo $JSON_DOC | jq # memory-impact-target-branch is allowed to fail without blocking CI.
exit 1 # This job builds the target branch (dev/beta/release) which may fail because:
# 1. The target branch has a build issue independent of this PR
# 2. This PR fixes a build issue on the target branch
# In either case, we only care that the PR branch builds successfully.
echo "$NEEDS_JSON" | jq -e 'del(.["memory-impact-target-branch"]) | all(.result != "failure")'

View File

@@ -1,5 +1,6 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
THIS IS A SYNTAX ERROR !
#include <cinttypes> #include <cinttypes>
#include <limits> #include <limits>
#include <memory> #include <memory>
@@ -313,13 +314,17 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std:
float backoff_increase_factor) { // NOLINT float backoff_increase_factor) { // NOLINT
App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
} }
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } bool Component::is_failed() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED;
}
bool Component::is_ready() const { bool Component::is_ready() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE ||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
} }
bool Component::is_idle() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE; } bool Component::is_idle() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE;
}
bool Component::can_proceed() { return true; } bool Component::can_proceed() { return true; }
bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; } bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; }
bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; }

View File

@@ -215,6 +215,20 @@ def prepare_symbol_changes_data(
} }
def format_components_str(components: list[str]) -> str:
"""Format a list of components for display.
Args:
components: List of component names
Returns:
Formatted string with backtick-quoted component names
"""
if len(components) == 1:
return f"`{components[0]}`"
return ", ".join(f"`{c}`" for c in sorted(components))
def prepare_component_breakdown_data( def prepare_component_breakdown_data(
target_analysis: dict | None, pr_analysis: dict | None target_analysis: dict | None, pr_analysis: dict | None
) -> list[tuple[str, int, int, int]] | None: ) -> list[tuple[str, int, int, int]] | None:
@@ -316,11 +330,10 @@ def create_comment_body(
} }
# Format components list # Format components list
context["components_str"] = format_components_str(components)
if len(components) == 1: if len(components) == 1:
context["components_str"] = f"`{components[0]}`"
context["config_note"] = "a representative test configuration" context["config_note"] = "a representative test configuration"
else: else:
context["components_str"] = ", ".join(f"`{c}`" for c in sorted(components))
context["config_note"] = ( context["config_note"] = (
f"a merged configuration with {len(components)} components" f"a merged configuration with {len(components)} components"
) )
@@ -502,6 +515,43 @@ def post_or_update_comment(pr_number: str, comment_body: str) -> None:
print("Comment posted/updated successfully", file=sys.stderr) print("Comment posted/updated successfully", file=sys.stderr)
def create_target_unavailable_comment(
pr_data: dict,
) -> str:
"""Create a comment body when target branch data is unavailable.
This happens when the target branch (dev/beta/release) fails to build.
This can occur because:
1. The target branch has a build issue independent of this PR
2. This PR fixes a build issue on the target branch
In either case, we only care that the PR branch builds successfully.
Args:
pr_data: Dictionary with PR branch analysis results
Returns:
Formatted comment body
"""
components = pr_data.get("components", [])
platform = pr_data.get("platform", "unknown")
pr_ram = pr_data.get("ram_bytes", 0)
pr_flash = pr_data.get("flash_bytes", 0)
env = Environment(
loader=FileSystemLoader(TEMPLATE_DIR),
trim_blocks=True,
lstrip_blocks=True,
)
template = env.get_template("ci_memory_impact_target_unavailable.j2")
return template.render(
comment_marker=COMMENT_MARKER,
components_str=format_components_str(components),
platform=platform,
pr_ram=format_bytes(pr_ram),
pr_flash=format_bytes(pr_flash),
)
def main() -> int: def main() -> int:
"""Main entry point.""" """Main entry point."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
@@ -523,15 +573,25 @@ def main() -> int:
# Load analysis JSON files (all data comes from JSON for security) # Load analysis JSON files (all data comes from JSON for security)
target_data: dict | None = load_analysis_json(args.target_json) target_data: dict | None = load_analysis_json(args.target_json)
if not target_data:
print("Error: Failed to load target analysis JSON", file=sys.stderr)
sys.exit(1)
pr_data: dict | None = load_analysis_json(args.pr_json) pr_data: dict | None = load_analysis_json(args.pr_json)
# PR data is required - if the PR branch can't build, that's a real error
if not pr_data: if not pr_data:
print("Error: Failed to load PR analysis JSON", file=sys.stderr) print("Error: Failed to load PR analysis JSON", file=sys.stderr)
sys.exit(1) sys.exit(1)
# Target data is optional - target branch (dev) may fail to build because:
# 1. The target branch has a build issue independent of this PR
# 2. This PR fixes a build issue on the target branch
if not target_data:
print(
"Warning: Target branch analysis unavailable, posting limited comment",
file=sys.stderr,
)
comment_body = create_target_unavailable_comment(pr_data)
post_or_update_comment(args.pr_number, comment_body)
return 0
# Extract detailed analysis if available # Extract detailed analysis if available
target_analysis: dict | None = None target_analysis: dict | None = None
pr_analysis: dict | None = None pr_analysis: dict | None = None

View File

@@ -0,0 +1,19 @@
{{ comment_marker }}
## Memory Impact Analysis
**Components:** {{ components_str }}
**Platform:** `{{ platform }}`
| Metric | This PR |
|--------|---------|
| **RAM** | {{ pr_ram }} |
| **Flash** | {{ pr_flash }} |
> ⚠️ **Target branch comparison unavailable** - The target branch failed to build.
> This can happen when the target branch has a build issue, or when this PR fixes a build issue on the target branch.
> The PR branch compiled successfully with the memory usage shown above.
---
> **Note:** This analysis measures **static RAM and Flash usage** only (compile-time allocation).
*This analysis runs automatically when components change.*