Skip to content

Commit f26b07e

Browse files
committed
fix(hacs): drop zip_release from hacs.json - it broke every install
hacs.json carried "zip_release": true without a "filename". That is the combination HACS rejects ("zip_release is True, but filename is not set"), and the download step then called endswith on the missing filename, so every install and update failed with Failed to perform the action update/install. 'NoneType' object has no attribute 'endswith' The key came from the old master line (added 2026-09-14) and survived the merge into the 2.1.0 release because the development branch had changed a different line of the file, so git merged both changes without a conflict. The key is not needed at all: an integration ships its source rather than a release asset, so "zip_release" and "content_in_root" are gone. Adds tests/verify_hacs_manifest.py, which validates hacs.json and manifest.json against HACS's own schemas - including the zip_release rule - and runs it in CI. "ci: replace the generic Python application workflow with the real harnesses The workflow that shipped with the repository pinned Python 3.10 and ran pytest over a repository with no pytest tests, so it could only ever fail. .github/workflows/verify.yml runs test_crow_protocol.py and the two stub-HA harnesses on Python 3.14 instead.
1 parent c8e1241 commit f26b07e

4 files changed

Lines changed: 177 additions & 3 deletions

File tree

‎.github/workflows/verify.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,6 @@ jobs:
4343

4444
- name: Config flow and translations
4545
run: python tests/verify_config_flow.py
46+
47+
- name: HACS metadata (hacs.json + manifest.json)
48+
run: python tests/verify_hacs_manifest.py

‎CHANGELOG.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,23 @@ Home Assistant 2026.8 or older should stay on `2.0.0`.
4848
repository is replaced by `.github/workflows/verify.yml`. The old one pinned
4949
Python 3.10 (this integration targets Home Assistant 2026.9, which requires
5050
3.14.2+) and ran `pytest` over a repository that contains no pytest tests, so
51-
it could only ever fail. The new workflow runs the three real harnesses on
51+
it could only ever fail. The new workflow runs the project's harnesses on
5252
Python 3.14.
53+
* **`hacs.json` no longer sets `zip_release`.** The key was inherited from the
54+
old `master` line (added there on 2026-09-14) and had no `filename` next to
55+
it. HACS rejects exactly that combination - "zip_release is True, but
56+
filename is not set" - and the download step then called `endswith` on the
57+
missing filename, so **every** install and update failed with
58+
`Failed to perform the action update/install. 'NoneType' object has no
59+
attribute 'endswith'`. An integration ships its source rather than a release
60+
asset, so the key is not needed at all. `tests/verify_hacs_manifest.py` now
61+
validates `hacs.json` and `manifest.json` against HACS's own schemas, and CI
62+
runs it, so this cannot come back unnoticed.
63+
64+
> The `2.1.0` tag was moved onto this fix and the release was published
65+
> again. HACS downloads by tag, so a repository that was already fetched needs
66+
> *Redownload* (or *Update*, if HACS still offers it) after refreshing the
67+
> repository list.
5368
5469
## [2.1.0-beta.2] - 2026-09-21
5570

‎hacs.json‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
{
22
"name": "Crow/AAP Alarm IP Module",
3-
"content_in_root": false,
4-
"zip_release": true,
53
"render_readme": true,
64
"homeassistant": "2026.9.3"
75
}

‎tests/verify_hacs_manifest.py‎

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""Validate the HACS metadata of this repository.
2+
3+
`hacs.json` is not just documentation: HACS parses it with the schema below and
4+
refuses to install a repository that violates it. The 2.1.0 release shipped a
5+
`hacs.json` inherited from the old `master` line that contained
6+
7+
"zip_release": true
8+
9+
without a `filename`, which HACS rejects ("zip_release is True, but filename is
10+
not set") and which made installation fail with
11+
12+
Failed to perform the action update/install. 'NoneType' object has no
13+
attribute 'endswith'
14+
15+
because the download step then calls `filename.endswith(...)`.
16+
17+
The schemas are copies of HACS's own
18+
(`custom_components/hacs/utils/validate.py` -> `HACS_MANIFEST_JSON_SCHEMA`,
19+
`INTEGRATION_MANIFEST_JSON_SCHEMA`, and
20+
`custom_components/hacs/validate/hacsjson.py`). Keep them in sync when HACS
21+
changes them.
22+
23+
Run: python tests/verify_hacs_manifest.py
24+
"""
25+
26+
from __future__ import annotations
27+
28+
import json
29+
import pathlib
30+
import sys
31+
32+
REPO = pathlib.Path(__file__).resolve().parent.parent
33+
34+
# The harness dependency lives in the (gitignored) vendor folder, like in
35+
# tests/verify_config_flow.py.
36+
sys.path.insert(0, str(REPO / "tests" / "vendor"))
37+
38+
try:
39+
import voluptuous as vol
40+
except ImportError:
41+
sys.exit(
42+
"voluptuous is required: python -m pip install --target tests/vendor voluptuous"
43+
)
44+
45+
HACS_JSON = REPO / "hacs.json"
46+
MANIFEST_JSON = REPO / "custom_components" / "crowipmodule" / "manifest.json"
47+
INFO_FILES = ("readme.md", "readme", "info.md", "info")
48+
49+
# --- copied from HACS: custom_components/hacs/utils/validate.py ---------------
50+
# Allowed keys. `extra=vol.PREVENT_EXTRA` means an unknown key is an ERROR, so
51+
# inherited keys from other ecosystems (iot_class, domains, ...) must not linger.
52+
HACS_MANIFEST_JSON_SCHEMA = vol.Schema(
53+
{
54+
vol.Optional("content_in_root"): bool,
55+
vol.Optional("country"): vol.Any(list, str),
56+
vol.Optional("filename"): str,
57+
vol.Optional("hacs"): str,
58+
vol.Optional("hide_default_branch"): bool,
59+
vol.Optional("homeassistant"): str,
60+
vol.Optional("persistent_directory"): str,
61+
vol.Optional("render_readme"): bool,
62+
vol.Optional("zip_release"): bool,
63+
vol.Required("name"): str,
64+
},
65+
extra=vol.PREVENT_EXTRA,
66+
)
67+
68+
INTEGRATION_MANIFEST_JSON_SCHEMA = vol.Schema(
69+
{
70+
vol.Required("codeowners"): list,
71+
vol.Required("documentation"): str,
72+
vol.Required("domain"): str,
73+
vol.Required("issue_tracker"): str,
74+
vol.Required("name"): str,
75+
vol.Required("version"): str,
76+
},
77+
extra=vol.ALLOW_EXTRA,
78+
)
79+
# -----------------------------------------------------------------------------
80+
81+
FAILURES: list[str] = []
82+
CHECKS = 0
83+
84+
85+
def check(condition: bool, message: str) -> None:
86+
global CHECKS
87+
CHECKS += 1
88+
if condition:
89+
print(f"[PASS] {message}")
90+
else:
91+
print(f"[FAIL] {message}")
92+
FAILURES.append(message)
93+
94+
95+
def main() -> int:
96+
if not HACS_JSON.is_file():
97+
sys.exit("hacs.json is missing - HACS would report the repository as invalid")
98+
99+
raw = json.loads(HACS_JSON.read_text(encoding="utf-8"))
100+
print(f"hacs.json = {json.dumps(raw, indent=2)}\n")
101+
102+
try:
103+
HACS_MANIFEST_JSON_SCHEMA(raw)
104+
check(True, "hacs.json matches HACS_MANIFEST_JSON_SCHEMA (no unknown keys)")
105+
except vol.Invalid as err:
106+
check(False, f"hacs.json violates the HACS schema: {err}")
107+
108+
# The rule that broke 2.1.0 (validate/hacsjson.py, INTEGRATION category).
109+
check(
110+
not (raw.get("zip_release") and not raw.get("filename")),
111+
"zip_release is not set without a filename",
112+
)
113+
check(
114+
not raw.get("zip_release"),
115+
"zip_release is not used at all (this integration ships its source, "
116+
"not a release asset)",
117+
)
118+
check(raw.get("name") == "Crow/AAP Alarm IP Module", "hacs.json name matches the repository")
119+
check("homeassistant" in raw, "hacs.json pins a minimum Home Assistant version")
120+
121+
# A repository must have a readme or an info.md (validate/information.py).
122+
files = [p.name.lower() for p in REPO.iterdir()]
123+
check(
124+
any(name in files for name in INFO_FILES),
125+
"the repository has a readme/info file",
126+
)
127+
check((REPO / "LICENSE").is_file(), "the repository has a LICENSE (validate/license.py)")
128+
129+
manifest = json.loads(MANIFEST_JSON.read_text(encoding="utf-8"))
130+
try:
131+
INTEGRATION_MANIFEST_JSON_SCHEMA(manifest)
132+
check(True, "manifest.json matches INTEGRATION_MANIFEST_JSON_SCHEMA")
133+
except vol.Invalid as err:
134+
check(False, f"manifest.json violates the HACS schema: {err}")
135+
136+
check(manifest.get("domain") == "crowipmodule", "manifest domain is crowipmodule")
137+
check(
138+
(REPO / "custom_components" / manifest["domain"]).is_dir(),
139+
f"custom_components/{manifest.get('domain')}/ exists",
140+
)
141+
142+
# HACS compares the release tag against the manifest version.
143+
tag = manifest["version"]
144+
check(
145+
raw.get("hacs", tag) == tag,
146+
f"manifest version {tag!r} is what HACS will compare against the tag",
147+
)
148+
149+
print("\n" + "=" * 40)
150+
if FAILURES:
151+
print(f"{len(FAILURES)} of {CHECKS} checks FAILED")
152+
return 1
153+
print(f"ALL {CHECKS} CHECKS PASSED")
154+
return 0
155+
156+
157+
if __name__ == "__main__":
158+
raise SystemExit(main())

0 commit comments

Comments
 (0)