Repository: home-assistant/core
Pull request #143526: Remove add-on changelog from cached information
Author: login: agners
Labels: name: core
name: breaking-change
name: cla-signed
name: integration: hassio
name: small-pr
description: PRs with less than 30 lines.
name: by-code-owner
name: Quality Scale: internal
Created: 2025-04-23T17:17:05Z | Merged: 2025-04-24T20:14:46Z

<!--
  You are amazing! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template! (unless instructed).
-->
## Breaking change

The state attribute `release_summary` is no longer provided for add-on update entities. The attribute was auto generated from the release notes and often ended up incomplete. It lacked any structural guarantees and hence was not useful for automatons.

<!--
  If your PR contains a breaking change for existing users, it is important
  to tell them what breaks, how to make it work again and why we did this.
  This piece of text is published with the release notes, so it helps if you
  write it towards our users, not us.
  Note: Remove this section if this PR is NOT a breaking change.
-->


## Proposed change
The add-on changelog currently regularly fetched and are stored in memory for all add-ons However, the `release_summary` is only a shortened version of the change-log, potentially not even the relevant part (depending on CHANGELOG.md formatting) and not that useful. The full release notes are now loaded on demand when the release notes is actually required. This lowers update time and memory footprint of the hassio integration.

<!--
  Describe the big picture of your changes here to communicate to the
  maintainers why we should accept this pull request. If it fixes a bug
  or resolves a feature request, be sure to link to that issue in the
  additional information section.
-->


## Type of change
<!--
  What type of change does your PR introduce to Home Assistant?
  NOTE: Please, check only 1! box!
  If your PR requires multiple boxes to be checked, you'll most likely need to
  split it into multiple PRs. This makes things easier and faster to code review.
-->

- [ ] Dependency upgrade
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New integration (thank you!)
- [ ] New feature (which adds functionality to an existing integration)
- [ ] Deprecation (breaking change to happen in the future)
- [x] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests

## Additional information
<!--
  Details are important, and help maintainers processing your PR.
  Please be sure to fill out additional details, if applicable.
-->

- This PR fixes or closes issue: fixes #
- This PR is related to issue: 
- Link to documentation pull request: 
- Link to developer documentation pull request: 
- Link to frontend pull request: 

## Checklist
<!--
  Put an `x` in the boxes that apply. You can also fill these out after
  creating the PR. If you're unsure about any of them, don't hesitate to ask.
  We're here to help! This is simply a reminder of what we are going to look
  for before merging your code.
-->

- [ ] The code change is tested and works locally.
- [ ] Local tests pass. **Your PR cannot be merged unless tests pass**
- [ ] There is no commented out code in this PR.
- [ ] I have followed the [development checklist][dev-checklist]
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
- [ ] The code has been formatted using Ruff (`ruff format homeassistant tests`)
- [ ] Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]

If the code communicates with devices, web services, or third-party tools:

- [ ] The [manifest file][manifest-docs] has all fields filled out correctly.  
      Updated and included derived files by running: `python3 -m script.hassfest`.
- [ ] New or updated dependencies have been added to `requirements_all.txt`.  
      Updated by running `python3 -m script.gen_requirements_all`.
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.

<!--
  This project is very active and we have a high turnover of pull requests.

  Unfortunately, the number of incoming pull requests is higher than what our
  reviewers can review and merge so there is a long backlog of pull requests
  waiting for review. You can help here!
  
  By reviewing another pull request, you will help raise the code quality of
  that pull request and the final review will be faster. This way the general
  pace of pull request reviews will go up and your wait time will go down.
  
  When picking a pull request to review, try to choose one that hasn't yet
  been reviewed.

  Thanks for helping out!
-->

To help with the load of incoming pull requests:

- [ ] I have reviewed two other [open pull requests][prs] in this repository.

[prs]: https://github.com/home-assistant/core/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+-label%3Awaiting-for-upstream+sort%3Acreated-desc+review%3Anone+-status%3Afailure

<!--
  Thank you for contributing <3

  Below, some useful links you could explore:
-->
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
[manifest-docs]: https://developers.home-assistant.io/docs/creating_integration_manifest/
[quality-scale]: https://developers.home-assistant.io/docs/integration_quality_scale_index/
[docs-repository]: https://github.com/home-assistant/home-assistant.io
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr



Diff:
diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py
index bc0f819fde9e17..3eef1c14dd0167 100644
--- a/homeassistant/components/hassio/__init__.py
+++ b/homeassistant/components/hassio/__init__.py
@@ -104,7 +104,6 @@
 )
 from .coordinator import (
     HassioDataUpdateCoordinator,
-    get_addons_changelogs,  # noqa: F401
     get_addons_info,
     get_addons_stats,  # noqa: F401
     get_core_info,  # noqa: F401
diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py
index 562669f674acf7..563b271c5788fe 100644
--- a/homeassistant/components/hassio/const.py
+++ b/homeassistant/components/hassio/const.py
@@ -85,7 +85,6 @@
 DATA_NETWORK_INFO = "hassio_network_info"
 DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
 DATA_SUPERVISOR_STATS = "hassio_supervisor_stats"
-DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs"
 DATA_ADDONS_INFO = "hassio_addons_info"
 DATA_ADDONS_STATS = "hassio_addons_stats"
 HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
@@ -94,7 +93,6 @@
 ATTR_VERSION = "version"
 ATTR_VERSION_LATEST = "version_latest"
 ATTR_CPU_PERCENT = "cpu_percent"
-ATTR_CHANGELOG = "changelog"
 ATTR_LOCATION = "location"
 ATTR_MEMORY_PERCENT = "memory_percent"
 ATTR_SLUG = "slug"
@@ -124,14 +122,13 @@
 SUPERVISOR_CONTAINER = "hassio_supervisor"
 
 CONTAINER_STATS = "stats"
-CONTAINER_CHANGELOG = "changelog"
 CONTAINER_INFO = "info"
 
 # This is a mapping of which endpoint the key in the addon data
 # is obtained from so we know which endpoint to update when the
 # coordinator polls for updates.
 KEY_TO_UPDATE_TYPES: dict[str, set[str]] = {
-    ATTR_VERSION_LATEST: {CONTAINER_INFO, CONTAINER_CHANGELOG},
+    ATTR_VERSION_LATEST: {CONTAINER_INFO},
     ATTR_MEMORY_PERCENT: {CONTAINER_STATS},
     ATTR_CPU_PERCENT: {CONTAINER_STATS},
     ATTR_VERSION: {CONTAINER_INFO},
diff --git a/homeassistant/components/hassio/coordinator.py b/homeassistant/components/hassio/coordinator.py
index 833068a713c1ca..25a0e1dd6b2436 100644
--- a/homeassistant/components/hassio/coordinator.py
+++ b/homeassistant/components/hassio/coordinator.py
@@ -7,7 +7,7 @@
 import logging
 from typing import TYPE_CHECKING, Any
 
-from aiohasupervisor import SupervisorError
+from aiohasupervisor import SupervisorError, SupervisorNotFoundError
 from aiohasupervisor.models import StoreInfo
 
 from homeassistant.config_entries import ConfigEntry
@@ -21,18 +21,15 @@
 
 from .const import (
     ATTR_AUTO_UPDATE,
-    ATTR_CHANGELOG,
     ATTR_REPOSITORY,
     ATTR_SLUG,
     ATTR_STARTED,
     ATTR_STATE,
     ATTR_URL,
     ATTR_VERSION,
-    CONTAINER_CHANGELOG,
     CONTAINER_INFO,
     CONTAINER_STATS,
     CORE_CONTAINER,
-    DATA_ADDONS_CHANGELOGS,
     DATA_ADDONS_INFO,
     DATA_ADDONS_STATS,
     DATA_COMPONENT,
@@ -155,16 +152,6 @@ def get_supervisor_stats(hass: HomeAssistant) -> dict[str, Any]:
     return hass.data.get(DATA_SUPERVISOR_STATS) or {}
 
 
-@callback
-@bind_hass
-def get_addons_changelogs(hass: HomeAssistant):
-    """Return Addons changelogs.
-
-    Async friendly.
-    """
-    return hass.data.get(DATA_ADDONS_CHANGELOGS)
-
-
 @callback
 @bind_hass
 def get_os_info(hass: HomeAssistant) -> dict[str, Any] | None:
@@ -337,7 +324,6 @@ async def _async_update_data(self) -> dict[str, Any]:
         supervisor_info = get_supervisor_info(self.hass) or {}
         addons_info = get_addons_info(self.hass) or {}
         addons_stats = get_addons_stats(self.hass)
-        addons_changelogs = get_addons_changelogs(self.hass)
         store_data = get_store(self.hass)
 
         if store_data:
@@ -355,7 +341,6 @@ async def _async_update_data(self) -> dict[str, Any]:
                 ATTR_AUTO_UPDATE: (addons_info.get(addon[ATTR_SLUG]) or {}).get(
                     ATTR_AUTO_UPDATE, False
                 ),
-                ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]),
                 ATTR_REPOSITORY: repositories.get(
                     addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "")
                 ),
@@ -427,6 +412,13 @@ async def force_info_update_supervisor(self) -> None:
         self.hass.data[DATA_SUPERVISOR_INFO] = await self.hassio.get_supervisor_info()
         await self.async_refresh()
 
+    async def get_changelog(self, addon_slug: str) -> str | None:
+        """Get the changelog for an add-on."""
+        try:
+            return await self.supervisor_client.store.addon_changelog(addon_slug)
+        except SupervisorNotFoundError:
+            return None
+
     async def force_data_refresh(self, first_update: bool) -> None:
         """Force update of the addon info."""
         container_updates = self._container_updates
@@ -475,13 +467,6 @@ async def force_data_refresh(self, first_update: bool) -> None:
                 started_addons,
                 False,
             ),
-            (
-                DATA_ADDONS_CHANGELOGS,
-                self._update_addon_changelog,
-                CONTAINER_CHANGELOG,
-                all_addons,
-                True,
-            ),
             (
                 DATA_ADDONS_INFO,
                 self._update_addon_info,
@@ -513,15 +498,6 @@ async def _update_addon_stats(self, slug: str) -> tuple[str, dict[str, Any] | No
             return (slug, None)
         return (slug, stats.to_dict())
 
-    async def _update_addon_changelog(self, slug: str) -> tuple[str, str | None]:
-        """Return the changelog for an add-on."""
-        try:
-            changelog = await self.supervisor_client.store.addon_changelog(slug)
-        except SupervisorError as err:
-            _LOGGER.warning("Could not fetch changelog for %s: %s", slug, err)
-            return (slug, None)
-        return (slug, changelog)
-
     async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]:
         """Return the info for an add-on."""
         try:
diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py
index 2c325979210054..bb1d3f8bd507cb 100644
--- a/homeassistant/components/hassio/update.py
+++ b/homeassistant/components/hassio/update.py
@@ -2,6 +2,7 @@
 
 from __future__ import annotations
 
+import re
 from typing import Any
 
 from aiohasupervisor import SupervisorError
@@ -21,7 +22,6 @@
 from .const import (
     ADDONS_COORDINATOR,
     ATTR_AUTO_UPDATE,
-    ATTR_CHANGELOG,
     ATTR_VERSION,
     ATTR_VERSION_LATEST,
     DATA_KEY_ADDONS,
@@ -116,11 +116,6 @@ def installed_version(self) -> str | None:
         """Version installed and in use."""
         return self._addon_data[ATTR_VERSION]
 
-    @property
-    def release_summary(self) -> str | None:
-        """Release summary for the add-on."""
-        return self._strip_release_notes()
-
     @property
     def entity_picture(self) -> str | None:
         """Return the icon of the add-on if any."""
@@ -130,27 +125,22 @@ def entity_picture(self) -> str | None:
             return f"/api/hassio/addons/{self._addon_slug}/icon"
         return None
 
-    def _strip_release_notes(self) -> str | None:
-        """Strip the release notes to contain the needed sections."""
-        if (notes := self._addon_data[ATTR_CHANGELOG]) is None:
+    async def async_release_notes(self) -> str | None:
+        """Return the release notes for the update."""
+        if (
+            changelog := await self.coordinator.get_changelog(self._addon_slug)
+        ) is None:
             return None
 
-        if (
-            f"# {self.latest_version}" in notes
-            and f"# {self.installed_version}" in notes
-        ):
-            # Split the release notes to only what is between the versions if we can
-            new_notes = notes.split(f"# {self.installed_version}")[0]
-            if f"# {self.latest_version}" in new_notes:
-                # Make sure the latest version is still there.
-                # This can be False if the order of the release notes are not correct
-                # In that case we just return the whole release notes
-                return new_notes
-        return notes
+        if self.latest_version is None or self.installed_version is None:
+            return changelog
 
-    async def async_release_notes(self) -> str | None:
-        """Return the release notes for the update."""
-        return self._strip_release_notes()
+        regex_pattern = re.compile(
+            rf"^#* {re.escape(self.latest_version)}\n(?:^(?!#* {re.escape(self.installed_version)}).*\n)*",
+            re.MULTILINE,
+        )
+        match = regex_pattern.search(changelog)
+        return match.group(0) if match else changelog
 
     async def async_install(
         self,
diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py
index e6699cfe68e56d..2ac06b46fcaf69 100644
--- a/tests/components/hassio/test_init.py
+++ b/tests/components/hassio/test_init.py
@@ -228,7 +228,7 @@ async def test_setup_api_ping(
         await hass.async_block_till_done()
 
     assert result
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
     assert get_core_info(hass)["version_latest"] == "1.0.0"
     assert is_hassio(hass)
 
@@ -275,7 +275,7 @@ async def test_setup_api_push_api_data(
         await hass.async_block_till_done()
 
     assert result
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
     assert not aioclient_mock.mock_calls[0][2]["ssl"]
     assert aioclient_mock.mock_calls[0][2]["port"] == 9999
     assert "watchdog" not in aioclient_mock.mock_calls[0][2]
@@ -296,7 +296,7 @@ async def test_setup_api_push_api_data_server_host(
         await hass.async_block_till_done()
 
     assert result
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
     assert not aioclient_mock.mock_calls[0][2]["ssl"]
     assert aioclient_mock.mock_calls[0][2]["port"] == 9999
     assert not aioclient_mock.mock_calls[0][2]["watchdog"]
@@ -317,7 +317,7 @@ async def test_setup_api_push_api_data_default(
         await hass.async_block_till_done()
 
     assert result
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
     assert not aioclient_mock.mock_calls[0][2]["ssl"]
     assert aioclient_mock.mock_calls[0][2]["port"] == 8123
     refresh_token = aioclient_mock.mock_calls[0][2]["refresh_token"]
@@ -398,7 +398,7 @@ async def test_setup_api_existing_hassio_user(
         await hass.async_block_till_done()
 
     assert result
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
     assert not aioclient_mock.mock_calls[0][2]["ssl"]
     assert aioclient_mock.mock_calls[0][2]["port"] == 8123
     assert aioclient_mock.mock_calls[0][2]["refresh_token"] == token.token
@@ -417,7 +417,7 @@ async def test_setup_core_push_timezone(
         await hass.async_block_till_done()
 
     assert result
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
     assert aioclient_mock.mock_calls[1][2]["timezone"] == "testzone"
 
     with patch("homeassistant.util.dt.set_default_time_zone"):
@@ -440,7 +440,7 @@ async def test_setup_hassio_no_additional_data(
         await hass.async_block_till_done()
 
     assert result
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
     assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456"
 
 
@@ -522,14 +522,14 @@ async def test_service_calls(
     )
     await hass.async_block_till_done()
 
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 24
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 22
     assert aioclient_mock.mock_calls[-1][2] == "test"
 
     await hass.services.async_call("hassio", "host_shutdown", {})
     await hass.services.async_call("hassio", "host_reboot", {})
     await hass.async_block_till_done()
 
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 26
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 24
 
     await hass.services.async_call("hassio", "backup_full", {})
     await hass.services.async_call(
@@ -544,7 +544,7 @@ async def test_service_calls(
     )
     await hass.async_block_till_done()
 
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 28
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 26
     assert aioclient_mock.mock_calls[-1][2] == {
         "name": "2021-11-13 03:48:00",
         "homeassistant": True,
@@ -569,7 +569,7 @@ async def test_service_calls(
     )
     await hass.async_block_till_done()
 
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 30
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 28
     assert aioclient_mock.mock_calls[-1][2] == {
         "addons": ["test"],
         "folders": ["ssl"],
@@ -588,7 +588,7 @@ async def test_service_calls(
     )
     await hass.async_block_till_done()
 
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 31
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 29
     assert aioclient_mock.mock_calls[-1][2] == {
         "name": "backup_name",
         "location": "backup_share",
@@ -604,7 +604,7 @@ async def test_service_calls(
     )
     await hass.async_block_till_done()
 
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 32
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 30
     assert aioclient_mock.mock_calls[-1][2] == {
         "name": "2021-11-13 03:48:00",
         "location": None,
@@ -623,7 +623,7 @@ async def test_service_calls(
     )
     await hass.async_block_till_done()
 
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 34
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 32
     assert aioclient_mock.mock_calls[-1][2] == {
         "name": "2021-11-13 11:48:00",
         "location": None,
@@ -1069,7 +1069,7 @@ async def test_setup_hardware_integration(
         await hass.async_block_till_done(wait_background_tasks=True)
 
     assert result
-    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20
+    assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
     assert len(mock_setup_entry.mock_calls) == 1
 
 
diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py
index b5f6dc96befd00..6ecc2b4424472c 100644
--- a/tests/components/hassio/test_update.py
+++ b/tests/components/hassio/test_update.py
@@ -5,7 +5,11 @@
 from typing import Any
 from unittest.mock import AsyncMock, MagicMock, patch
 
-from aiohasupervisor import SupervisorBadRequestError, SupervisorError
+from aiohasupervisor import (
+    SupervisorBadRequestError,
+    SupervisorError,
+    SupervisorNotFoundError,
+)
 from aiohasupervisor.models import (
     HomeAssistantUpdateOptions,
     OSUpdate,
@@ -987,6 +991,7 @@ async def test_update_core_with_backup_and_error(
 
 async def test_release_notes_between_versions(
     hass: HomeAssistant,
+    addon_changelog: AsyncMock,
     aioclient_mock: AiohttpClientMocker,
     hass_ws_client: WebSocketGenerator,
 ) -> None:
@@ -994,12 +999,10 @@ async def test_release_notes_between_versions(
     config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
     config_entry.add_to_hass(hass)
 
+    addon_changelog.return_value = "# 2.0.1\nNew updates\n# 2.0.0\nOld updates"
+
     with (
         patch.dict(os.environ, MOCK_ENVIRON),
-        patch(
-            "homeassistant.components.hassio.coordinator.get_addons_changelogs",
-            return_value={"test": "# 2.0.1\nNew updates\n# 2.0.0\nOld updates"},
-        ),
     ):
         result = await async_setup_component(
             hass,
@@ -1026,6 +1029,7 @@ async def test_release_notes_between_versions(
 
 async def test_release_notes_full(
     hass: HomeAssistant,
+    addon_changelog: AsyncMock,
     aioclient_mock: AiohttpClientMocker,
     hass_ws_client: WebSocketGenerator,
 ) -> None:
@@ -1033,12 +1037,11 @@ async def test_release_notes_full(
     config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
     config_entry.add_to_hass(hass)
 
+    full_changelog = "# 2.0.0\nNew updates\n# 2.0.0\nOld updates"
+    addon_changelog.return_value = full_changelog
+
     with (
         patch.dict(os.environ, MOCK_ENVIRON),
-        patch(
-            "homeassistant.components.hassio.coordinator.get_addons_changelogs",
-            return_value={"test": "# 2.0.0\nNew updates\n# 2.0.0\nOld updates"},
-        ),
     ):
         result = await async_setup_component(
             hass,
@@ -1062,9 +1065,21 @@ async def test_release_notes_full(
     assert "Old updates" in result["result"]
     assert "New updates" in result["result"]
 
+    # Update entity without update should returns full changelog
+    await client.send_json(
+        {
+            "id": 2,
+            "type": "update/release_notes",
+            "entity_id": "update.test2_update",
+        }
+    )
+    result = await client.receive_json()
+    assert result["result"] == full_changelog
+
 
 async def test_not_release_notes(
     hass: HomeAssistant,
+    addon_changelog: AsyncMock,
     aioclient_mock: AiohttpClientMocker,
     hass_ws_client: WebSocketGenerator,
 ) -> None:
@@ -1072,12 +1087,10 @@ async def test_not_release_notes(
     config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
     config_entry.add_to_hass(hass)
 
+    addon_changelog.side_effect = SupervisorNotFoundError()
+
     with (
         patch.dict(os.environ, MOCK_ENVIRON),
-        patch(
-            "homeassistant.components.hassio.coordinator.get_addons_changelogs",
-            return_value={"test": None},
-        ),
     ):
         result = await async_setup_component(
             hass,


Reviews:
user: login: bdraco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T17:33:21Z
user: login: agners
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T17:36:08Z
user: login: bdraco
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T17:45:39Z
user: login: bdraco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T17:50:01Z

Review comments:
diff_hunk: @@ -130,27 +126,15 @@ def entity_picture(self) -> str | None:
             return f"/api/hassio/addons/{self._addon_slug}/icon"
         return None
 
-    def _strip_release_notes(self) -> str | None:
-        """Strip the release notes to contain the needed sections."""
-        if (notes := self._addon_data[ATTR_CHANGELOG]) is None:
-            return None
-
-        if (
-            f"# {self.latest_version}" in notes
-            and f"# {self.installed_version}" in notes
-        ):
-            # Split the release notes to only what is between the versions if we can
-            new_notes = notes.split(f"# {self.installed_version}")[0]
-            if f"# {self.latest_version}" in new_notes:
-                # Make sure the latest version is still there.
-                # This can be False if the order of the release notes are not correct
-                # In that case we just return the whole release notes
-                return new_notes
-        return notes
-
     async def async_release_notes(self) -> str | None:
         """Return the release notes for the update."""
-        return self._strip_release_notes()
+        changelog = await self.coordinator.get_changelog(self._addon_slug)
+        regex_pattern = re.compile(
path: homeassistant/components/hassio/update.py
user: login: bdraco
body: I'd make the compiled regex a constant to avoid compiling it every time
created_at: 2025-04-23T17:33:10Z
author_association: MEMBER
diff_hunk: @@ -130,27 +126,15 @@ def entity_picture(self) -> str | None:
             return f"/api/hassio/addons/{self._addon_slug}/icon"
         return None
 
-    def _strip_release_notes(self) -> str | None:
-        """Strip the release notes to contain the needed sections."""
-        if (notes := self._addon_data[ATTR_CHANGELOG]) is None:
-            return None
-
-        if (
-            f"# {self.latest_version}" in notes
-            and f"# {self.installed_version}" in notes
-        ):
-            # Split the release notes to only what is between the versions if we can
-            new_notes = notes.split(f"# {self.installed_version}")[0]
-            if f"# {self.latest_version}" in new_notes:
-                # Make sure the latest version is still there.
-                # This can be False if the order of the release notes are not correct
-                # In that case we just return the whole release notes
-                return new_notes
-        return notes
-
     async def async_release_notes(self) -> str | None:
         """Return the release notes for the update."""
-        return self._strip_release_notes()
+        changelog = await self.coordinator.get_changelog(self._addon_slug)
+        regex_pattern = re.compile(
path: homeassistant/components/hassio/update.py
user: login: agners
body: It does include the versions as variables, so it can't be constant.

We could cache the regex though.

FWIW, if pre-compiled, the regex method is pretty much the same speed as the previous version. The advantage is that it cuts the release notes slightly cleaner (avoids left-over pound signs).
created_at: 2025-04-23T17:36:07Z
author_association: MEMBER
diff_hunk: @@ -130,27 +126,15 @@ def entity_picture(self) -> str | None:
             return f"/api/hassio/addons/{self._addon_slug}/icon"
         return None
 
-    def _strip_release_notes(self) -> str | None:
-        """Strip the release notes to contain the needed sections."""
-        if (notes := self._addon_data[ATTR_CHANGELOG]) is None:
-            return None
-
-        if (
-            f"# {self.latest_version}" in notes
-            and f"# {self.installed_version}" in notes
-        ):
-            # Split the release notes to only what is between the versions if we can
-            new_notes = notes.split(f"# {self.installed_version}")[0]
-            if f"# {self.latest_version}" in new_notes:
-                # Make sure the latest version is still there.
-                # This can be False if the order of the release notes are not correct
-                # In that case we just return the whole release notes
-                return new_notes
-        return notes
-
     async def async_release_notes(self) -> str | None:
         """Return the release notes for the update."""
-        return self._strip_release_notes()
+        changelog = await self.coordinator.get_changelog(self._addon_slug)
+        regex_pattern = re.compile(
path: homeassistant/components/hassio/update.py
user: login: bdraco
body: Its not called very often so unlikely to make a difference or worth the complexity of trying to cache it.
created_at: 2025-04-23T17:50:01Z
author_association: MEMBER

PR comments:
user: login: home-assistant[bot]
created_at: 2025-04-23T17:17:10Z
body: 
Hey there @home-assistant/supervisor, mind taking a look at this pull request as it has been labeled with an integration (`hassio`) you are listed as a [code owner](https://github.com/home-assistant/core/blob/dev/CODEOWNERS#L609) for? Thanks!

<details>
<summary>Code owner commands</summary>


Code owners of `hassio` can trigger bot actions by commenting:

- `@home-assistant close` Closes the pull request.
- `@home-assistant rename Awesome new title` Renames the pull request.
- `@home-assistant reopen` Reopen the pull request.
- `@home-assistant unassign hassio` Removes the current integration label and assignees on the pull request, add the integration domain after the command.
- `@home-assistant add-label needs-more-information` Add a label (needs-more-information, problem in dependency, problem in custom component) to the pull request.
- `@home-assistant remove-label needs-more-information` Remove a label (needs-more-information, problem in dependency, problem in custom component) on the pull request.

</details>


author_association: CONTRIBUTOR
user: login: agners
created_at: 2025-04-23T17:25:08Z
body: This is an attempt to optimize the Supervisor API load/overall handling of the changelog. Today the changelogs are fetched from file, so every update of the update coordinator (every 5 minutes) causes a file read for each add-on on Supervisor side. Typically these will likely be in page cache at that rate. This is pretty much the simpliest way to avoid those reads.

Alternatively, if we decide we need to keep the `release_summary`, it might be better to move the changelog read and parsing to Supervisor side.

Any thoughts?

(I'd address tests/formatting in case the general approach is agreed on)
author_association: MEMBER
user: login: bdraco
created_at: 2025-04-23T17:40:29Z
body: The frontend looks like it only uses the release summary if release notes are missing
https://github.com/home-assistant/frontend/blob/11b8f6210f509c8ed704ce27bb13a6e8a66f16dc/src/dialogs/more-info/controls/more-info-update.ts#L250

https://github.com/home-assistant/frontend/blob/11b8f6210f509c8ed704ce27bb13a6e8a66f16dc/src/dialogs/more-info/controls/more-info-update.ts#L264


author_association: MEMBER
user: login: bdraco
created_at: 2025-04-23T17:42:12Z
body: I don't see a strong reason to keep this. It's auto-generated from the release notes without meaningful summarization, so it mostly ends up as noise that no one will see or use. Given that it lacks any structural guarantees, I doubt anyone would write an automation based on it.

```
auto_update: false
display_precision: 0
installed_version: 2025.4.0
in_progress: false
latest_version: 2025.4.0
release_summary: >
  ## 2025.4.0


  ## Full list of changes


  ### New Components


  - Add support for MCP4461 quad i2c digipot/rheostat
  [esphome#8180](https://github.com/esphome/esphome/pull/8180) by
  [@p1ngb4ck](https://github.com/p1ngb4ck) (new-integration)


  ### Breaking Changes
release_url: null
skipped_version: null
title: ESPHome Device Builder
update_percentage: null
entity_picture: /api/hassio/addons/5c53de3b_esphome/icon
friendly_name: ESPHome Device Builder Update
supported_features: 25
```
```
auto_update: false
display_precision: 0
installed_version: 20.0.2
in_progress: false
latest_version: 20.0.2
release_summary: >-
  ## What’s changed


  ## ⬆️ Dependency updates


  - ⬆️ Update alpine_3_21/bind-tools to v9.18.34-r0
  @[renovate[bot]](https://github.com/apps/renovate)
  ([#858](https://github.com/hassio-addons/addon-ssh/pull/858))

  - ⬆️ Update alpine_3_21/mariadb-client to v11.4
release_url: null
skipped_version: null
title: Advanced SSH & Web Terminal
update_percentage: null
entity_picture: /api/hassio/addons/a0d7b954_ssh/icon
friendly_name: Advanced SSH & Web Terminal Update
supported_features: 25
```
author_association: MEMBER