From 48823669320c75fb7c9cefa3bfb8a4e188f0774f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 12:18:41 +0000 Subject: [PATCH 1/2] Add core_domain_override action validator Adds an integration-only action check that fails when the integration domain from manifest.json matches a Home Assistant core integration domain. The core domain list is read from the integrations.json file published by the Home Assistant site. HACS policy already states that integrations overriding a core integration are not accepted as defaults, but until now that was only enforced by a reviewer reading the hacs/default pull request. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Qc22QRVJF4ssjo7wPzBn2A --- .../hacs/validate/core_domain_override.py | 48 ++++++++++++++++ tests/action/test_hacs_action_integration.py | 6 +- .../next.home-assistant.io/integrations.json | 1 + .../bad_documentation.log | 3 +- .../bad_issue_tracker.log | 3 +- .../no_releases.log | 3 +- .../releases_without_assets.log | 3 +- .../valid_manifest.log | 3 +- ...-action-integration-bad-documentation.json | 1 + ...-action-integration-bad-issue-tracker.json | 1 + ...t-hacs-action-integration-no-releases.json | 1 + ...n-integration-releases-without-assets.json | 1 + ...acs-action-integration-valid-manifest.json | 1 + ...heckstest-async-run-repository-checks.json | 3 +- ...ktest-core-integrations-fetch-failure.json | 10 ++++ ...cktest-core-integrations-invalid-json.json | 10 ++++ ...override_checktest-domain-not-in-core.json | 10 ++++ ...hecktest-domain-overrides-core-domain.json | 10 ++++ ...rride_checktest-no-domain-skips-fetch.json | 9 +++ .../test_core_domain_override_check.py | 56 +++++++++++++++++++ 20 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 custom_components/hacs/validate/core_domain_override.py create mode 100644 tests/fixtures/proxy/next.home-assistant.io/integrations.json create mode 100644 tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-core-integrations-fetch-failure.json create mode 100644 tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-core-integrations-invalid-json.json create mode 100644 tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-domain-not-in-core.json create mode 100644 tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-domain-overrides-core-domain.json create mode 100644 tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-no-domain-skips-fetch.json create mode 100644 tests/validate/test_core_domain_override_check.py diff --git a/custom_components/hacs/validate/core_domain_override.py b/custom_components/hacs/validate/core_domain_override.py new file mode 100644 index 00000000000..62105a84034 --- /dev/null +++ b/custom_components/hacs/validate/core_domain_override.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..enums import HacsCategory +from ..utils.json import json_loads +from .base import ActionValidationBase, ValidationException + +if TYPE_CHECKING: + from ..repositories.base import HacsRepository + from ..repositories.integration import HacsIntegrationRepository + +# The `next` site is generated from the upcoming Home Assistant release, so domains +# landing in the next release are caught before they ship, unlike `www`. +CORE_INTEGRATIONS_URL = "https://next.home-assistant.io/integrations.json" + + +async def async_setup_validator(repository: HacsRepository) -> Validator: + """Set up this validator.""" + return Validator(repository=repository) + + +class Validator(ActionValidationBase): + """Validate the repository.""" + + repository: HacsIntegrationRepository + more_info = "https://hacs.xyz/docs/publish/include#check-core-domain-override" + categories = (HacsCategory.INTEGRATION,) + + async def async_validate(self) -> None: + """Validate the repository.""" + if not (domain := self.repository.data.domain): + # A missing or invalid manifest is reported by the integration_manifest check. + return + + result = await self.hacs.async_download_file(CORE_INTEGRATIONS_URL, handle_rate_limit=True) + if result is None: + raise ValidationException("Could not fetch the core integrations list") + + try: + core_domains = json_loads(result) + except Exception as err: + raise ValidationException("Could not parse the core integrations list") from err + + if domain in core_domains: + raise ValidationException( + f"The integration overrides the core integration domain '{domain}'" + ) diff --git a/tests/action/test_hacs_action_integration.py b/tests/action/test_hacs_action_integration.py index 355c7496717..8979661cfc2 100644 --- a/tests/action/test_hacs_action_integration.py +++ b/tests/action/test_hacs_action_integration.py @@ -86,6 +86,10 @@ async def test_hacs_action_integration( "https://brands.home-assistant.io/domains.json", response=MockedResponse(status=200, content={"custom": ["example"]}), ) + response_mocker.add( + "https://next.home-assistant.io/integrations.json", + response=MockedResponse(status=200, content=json.dumps({}), keep=True), + ) response_mocker.add( "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/custom_components/example/manifest.json", response=MockedResponse( @@ -110,7 +114,7 @@ async def test_hacs_action_integration( await preflight() assert ( - "All (9) checks passed" if test_case["succeed"] else "1/9 checks failed") in caplog.text + "All (10) checks passed" if test_case["succeed"] else "1/10 checks failed") in caplog.text splitlines = [f"<{line.rsplit(' <')[1]}" for line in caplog.text.split( "\n") if " <" in line] diff --git a/tests/fixtures/proxy/next.home-assistant.io/integrations.json b/tests/fixtures/proxy/next.home-assistant.io/integrations.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/tests/fixtures/proxy/next.home-assistant.io/integrations.json @@ -0,0 +1 @@ +{} diff --git a/tests/snapshots/action/test_hacs_action_integration/bad_documentation.log b/tests/snapshots/action/test_hacs_action_integration/bad_documentation.log index e20a2b2732b..6ee34d86601 100644 --- a/tests/snapshots/action/test_hacs_action_integration/bad_documentation.log +++ b/tests/snapshots/action/test_hacs_action_integration/bad_documentation.log @@ -5,6 +5,7 @@ Getting manifest.json for version=main completed completed + completed completed completed completed @@ -12,7 +13,7 @@ completed completed completed - 1/9 checks failed + 1/10 checks failed Validation completed ::group::data { diff --git a/tests/snapshots/action/test_hacs_action_integration/bad_issue_tracker.log b/tests/snapshots/action/test_hacs_action_integration/bad_issue_tracker.log index e2a3458a234..8c58168f29b 100644 --- a/tests/snapshots/action/test_hacs_action_integration/bad_issue_tracker.log +++ b/tests/snapshots/action/test_hacs_action_integration/bad_issue_tracker.log @@ -5,6 +5,7 @@ Getting manifest.json for version=main completed completed + completed completed completed completed @@ -12,7 +13,7 @@ completed completed completed - 1/9 checks failed + 1/10 checks failed Validation completed ::group::data { diff --git a/tests/snapshots/action/test_hacs_action_integration/no_releases.log b/tests/snapshots/action/test_hacs_action_integration/no_releases.log index c65f466cb27..f5d3f7f6bed 100644 --- a/tests/snapshots/action/test_hacs_action_integration/no_releases.log +++ b/tests/snapshots/action/test_hacs_action_integration/no_releases.log @@ -5,6 +5,7 @@ Getting manifest.json for version=main completed completed + completed completed completed completed @@ -12,7 +13,7 @@ completed completed completed - All (9) checks passed + All (10) checks passed Validation completed ::group::data { diff --git a/tests/snapshots/action/test_hacs_action_integration/releases_without_assets.log b/tests/snapshots/action/test_hacs_action_integration/releases_without_assets.log index 2ed0b2175f7..d9417024904 100644 --- a/tests/snapshots/action/test_hacs_action_integration/releases_without_assets.log +++ b/tests/snapshots/action/test_hacs_action_integration/releases_without_assets.log @@ -5,6 +5,7 @@ Getting manifest.json for version=main completed completed + completed completed completed completed @@ -12,7 +13,7 @@ completed completed completed - All (9) checks passed + All (10) checks passed Validation completed ::group::data { diff --git a/tests/snapshots/action/test_hacs_action_integration/valid_manifest.log b/tests/snapshots/action/test_hacs_action_integration/valid_manifest.log index 3647ca91f23..06c6ff4bb0f 100644 --- a/tests/snapshots/action/test_hacs_action_integration/valid_manifest.log +++ b/tests/snapshots/action/test_hacs_action_integration/valid_manifest.log @@ -5,6 +5,7 @@ Getting manifest.json for version=main completed completed + completed completed completed completed @@ -12,7 +13,7 @@ completed completed completed - All (9) checks passed + All (10) checks passed Validation completed ::group::data { diff --git a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-documentation.json b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-documentation.json index caf277160c2..6468674dc8a 100644 --- a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-documentation.json +++ b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-documentation.json @@ -6,6 +6,7 @@ "https://api.github.com/repos/hacs-test-org/integration-basic/git/trees/main": 1, "https://api.github.com/repos/hacs-test-org/integration-basic/releases": 1, "https://brands.home-assistant.io/domains.json": 1, + "https://next.home-assistant.io/integrations.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/custom_components/example/manifest.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1 } diff --git a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-issue-tracker.json b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-issue-tracker.json index 24745ef7f08..7f7e1775a4f 100644 --- a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-issue-tracker.json +++ b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-bad-issue-tracker.json @@ -6,6 +6,7 @@ "https://api.github.com/repos/hacs-test-org/integration-basic/git/trees/main": 1, "https://api.github.com/repos/hacs-test-org/integration-basic/releases": 1, "https://brands.home-assistant.io/domains.json": 1, + "https://next.home-assistant.io/integrations.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/custom_components/example/manifest.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1 } diff --git a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-no-releases.json b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-no-releases.json index 4cbc668ecb2..5b3b133e86c 100644 --- a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-no-releases.json +++ b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-no-releases.json @@ -6,6 +6,7 @@ "https://api.github.com/repos/hacs-test-org/integration-basic/git/trees/main": 1, "https://api.github.com/repos/hacs-test-org/integration-basic/releases": 1, "https://brands.home-assistant.io/domains.json": 1, + "https://next.home-assistant.io/integrations.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/custom_components/example/manifest.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1 } diff --git a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-releases-without-assets.json b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-releases-without-assets.json index 79429850924..b027fd15df7 100644 --- a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-releases-without-assets.json +++ b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-releases-without-assets.json @@ -6,6 +6,7 @@ "https://api.github.com/repos/hacs-test-org/integration-basic/git/trees/main": 1, "https://api.github.com/repos/hacs-test-org/integration-basic/releases": 1, "https://brands.home-assistant.io/domains.json": 1, + "https://next.home-assistant.io/integrations.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/custom_components/example/manifest.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1 } diff --git a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-valid-manifest.json b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-valid-manifest.json index 84542d92a4d..56d9d33d855 100644 --- a/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-valid-manifest.json +++ b/tests/snapshots/api-usage/tests/action/test_hacs_action_integrationtest-hacs-action-integration-valid-manifest.json @@ -6,6 +6,7 @@ "https://api.github.com/repos/hacs-test-org/integration-basic/git/trees/main": 1, "https://api.github.com/repos/hacs-test-org/integration-basic/releases": 1, "https://brands.home-assistant.io/domains.json": 1, + "https://next.home-assistant.io/integrations.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/custom_components/example/manifest.json": 1, "https://raw.githubusercontent.com/hacs-test-org/integration-basic/main/hacs.json": 1 } diff --git a/tests/snapshots/api-usage/tests/validate/test_async_run_repository_checkstest-async-run-repository-checks.json b/tests/snapshots/api-usage/tests/validate/test_async_run_repository_checkstest-async-run-repository-checks.json index 3dea3402bee..c4bc796d2de 100644 --- a/tests/snapshots/api-usage/tests/validate/test_async_run_repository_checkstest-async-run-repository-checks.json +++ b/tests/snapshots/api-usage/tests/validate/test_async_run_repository_checkstest-async-run-repository-checks.json @@ -5,6 +5,7 @@ "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, "https://api.github.com/repos/hacs/integration/git/trees/main": 1, "https://api.github.com/repos/hacs/integration/releases": 1, - "https://brands.home-assistant.io/domains.json": 1 + "https://brands.home-assistant.io/domains.json": 1, + "https://next.home-assistant.io/integrations.json": 1 } } \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-core-integrations-fetch-failure.json b/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-core-integrations-fetch-failure.json new file mode 100644 index 00000000000..53c2ffb149f --- /dev/null +++ b/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-core-integrations-fetch-failure.json @@ -0,0 +1,10 @@ +{ + "tests/validate/test_core_domain_override_check.py::test_core_integrations_fetch_failure": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1, + "https://next.home-assistant.io/integrations.json": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-core-integrations-invalid-json.json b/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-core-integrations-invalid-json.json new file mode 100644 index 00000000000..86aebbc0caf --- /dev/null +++ b/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-core-integrations-invalid-json.json @@ -0,0 +1,10 @@ +{ + "tests/validate/test_core_domain_override_check.py::test_core_integrations_invalid_json": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1, + "https://next.home-assistant.io/integrations.json": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-domain-not-in-core.json b/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-domain-not-in-core.json new file mode 100644 index 00000000000..d9657635362 --- /dev/null +++ b/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-domain-not-in-core.json @@ -0,0 +1,10 @@ +{ + "tests/validate/test_core_domain_override_check.py::test_domain_not_in_core": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1, + "https://next.home-assistant.io/integrations.json": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-domain-overrides-core-domain.json b/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-domain-overrides-core-domain.json new file mode 100644 index 00000000000..e8ce7dd7c98 --- /dev/null +++ b/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-domain-overrides-core-domain.json @@ -0,0 +1,10 @@ +{ + "tests/validate/test_core_domain_override_check.py::test_domain_overrides_core_domain": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1, + "https://next.home-assistant.io/integrations.json": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-no-domain-skips-fetch.json b/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-no-domain-skips-fetch.json new file mode 100644 index 00000000000..473080246b7 --- /dev/null +++ b/tests/snapshots/api-usage/tests/validate/test_core_domain_override_checktest-no-domain-skips-fetch.json @@ -0,0 +1,9 @@ +{ + "tests/validate/test_core_domain_override_check.py::test_no_domain_skips_fetch": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1 + } +} \ No newline at end of file diff --git a/tests/validate/test_core_domain_override_check.py b/tests/validate/test_core_domain_override_check.py new file mode 100644 index 00000000000..79255403767 --- /dev/null +++ b/tests/validate/test_core_domain_override_check.py @@ -0,0 +1,56 @@ +import json + +from custom_components.hacs.validate.core_domain_override import ( + CORE_INTEGRATIONS_URL, + Validator, +) + +from tests.common import MockedResponse, ResponseMocker + + +async def test_domain_not_in_core(repository, response_mocker: ResponseMocker): + response_mocker.add( + CORE_INTEGRATIONS_URL, + MockedResponse(content=json.dumps({"hue": {}})), + ) + repository.data.domain = "test" + check = Validator(repository) + await check.execute_validation() + assert not check.failed + + +async def test_domain_overrides_core_domain(repository, response_mocker: ResponseMocker): + response_mocker.add( + CORE_INTEGRATIONS_URL, + MockedResponse(content=json.dumps({"test": {}})), + ) + repository.data.domain = "test" + check = Validator(repository) + await check.execute_validation() + assert check.failed + + +async def test_core_integrations_fetch_failure(repository, response_mocker: ResponseMocker): + response_mocker.add(CORE_INTEGRATIONS_URL, MockedResponse(status=500)) + repository.data.domain = "test" + check = Validator(repository) + await check.execute_validation() + assert check.failed + + +async def test_core_integrations_invalid_json(repository, response_mocker: ResponseMocker): + response_mocker.add(CORE_INTEGRATIONS_URL, MockedResponse(content="not json")) + repository.data.domain = "test" + check = Validator(repository) + await check.execute_validation() + assert check.failed + + +async def test_no_domain_skips_fetch(repository, response_mocker: ResponseMocker): + # Without a domain there is nothing to compare, the missing manifest is + # reported by the integration_manifest check instead. + response_mocker.add(CORE_INTEGRATIONS_URL, MockedResponse(status=500)) + repository.data.domain = None + check = Validator(repository) + await check.execute_validation() + assert not check.failed From 25da5a12c7c444cfe4695816df51a97546bb2821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 31 Jul 2026 15:10:53 +0200 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- custom_components/hacs/validate/core_domain_override.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/hacs/validate/core_domain_override.py b/custom_components/hacs/validate/core_domain_override.py index 62105a84034..9e1748ccdd1 100644 --- a/custom_components/hacs/validate/core_domain_override.py +++ b/custom_components/hacs/validate/core_domain_override.py @@ -42,6 +42,9 @@ async def async_validate(self) -> None: except Exception as err: raise ValidationException("Could not parse the core integrations list") from err + if not isinstance(core_domains, dict): + raise ValidationException("Core integrations list has an unexpected format") + if domain in core_domains: raise ValidationException( f"The integration overrides the core integration domain '{domain}'"