PKG-09: refuse a deploy that would be a no-op, or that the add-on would reject
checks / unit (pull_request) Successful in 6m11s

Two ways a deploy of this add-on has gone wrong, and the Supervisor reports
both as success.

THE SILENT NO-OP. The Supervisor keys the built image by `version:`, so
`ha apps update` against an unchanged version installs nothing and says it
worked. PKG-06 recorded this on 2026-09-05 with four merges behind an unbumped
version. PKG-07 recorded it again on 2026-09-06 with six. Both times a human
noticed. Nothing else was going to.

THE REFUSED CONFIG. A config the shipped validator rejects is SystemExit(78) -
the add-on does not start. ⚠️ AND THE INGRESS PANEL GOES WITH IT, because the
add-on serving it is not running, so recovery means the Supervisor UI.
SAFETY-39's rule 10 made this reachable from a config that started fine on the
previous version: 0.7.0's own default `slew_w: 1000` exceeds the 555.6 W
ceiling on a 5000 W rail.

⚠️ WHY THIS IS NOT A CI CHECK, WHICH IS THE OBVIOUS IMPLEMENTATION. "A pull
request that touches runtime code must bump the version" is the wrong shape for
this project. It deliberately batches many merges into one deploy, so a per-PR
bump would put every open branch in conflict over one line of config.yaml, and
it still would not prevent the actual failure - which happens at DEPLOY, not at
merge. The check belongs where the mistake is made, so it is a script the
operator runs, not a gate on a pull request.

WHAT IT DOES NOT DO. It does not read the live box: the options blob needs the
Supervisor API and a supervisor token, and neither the HA REST API nor any
entity exposes it. The operator exports the options and passes --options.
Without that flag the validator half SKIPS - loudly, exit 2 - and never
pretends. `app/validate.py` imports its siblings and those need aiohttp, so on
a bare host that half skips too and says which container to re-run it in.

EXIT CODES: 0 cleared, 1 REFUSED, 2 NOT CLEARED. ⚠️ 2 IS NOT A PASS.

⚠️ THE TEST'S FIRST DRAFT REPORTED FOUR SILENT SKIPS AS PASSES. It copied the
gate into each throwaway fixture, so the script's sibling `app/` was the stub
the fixture had built and the validator import failed - every validator
assertion skipped, and four of them were written to expect a clearance. Fixed
by invoking the gate WHERE IT SHIPS and pointing --tree/--repo at the fixture,
which is also the behaviour that matters: the gate must check a config against
the validator it ships beside. That is MUTATION.md habit 4 again - a check
whose data cannot reach the failure it names - caught this time by the harness
positive control rather than in review.

Every refusal in test_version_gate.py is DRIVEN: a repository is built in which
the failure is really present and the check is required to say so. There is a
positive control that the no-op detector does NOT fire on a docs-only change,
and a pin that omitting --options can never exit 0.

This change needs no version bump, and the gate agrees: `tools/` and
`test_*.py` are outside its runtime set, because demanding a bump for a typo
fix trains people to bump without reading - the habit it exists to prevent.

Suite: 24 files, all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
glenn schrooyen
2026-09-06 22:07:55 +02:00
co-authored by Claude Opus 5
parent 3368231048
commit 09b84e30b0
3 changed files with 391 additions and 0 deletions
+43
View File
@@ -1452,3 +1452,46 @@ If an MQTT broker is available the add-on publishes setpoint, grid power,
battery power, circulating power, state of charge, maintenance phase and
controller status by MQTT discovery. This is observability only — the controller works fine without a
broker, and MQTT problems can never affect control.
## Before you deploy: `tools/predeploy_check.py` (PKG-09)
Two ways a deploy has gone wrong on this system, both of which the Supervisor
reports as success:
**The silent no-op.** The Supervisor keys the built image by `version:` in
`config.yaml`. `ha apps update` against an unchanged version installs nothing
and says it worked. This happened twice: PKG-06 (four merges behind an unbumped
version) and PKG-07 (six). Both times a human noticed. Nothing else would have.
**The refused config.** A config the shipped validator rejects is
`SystemExit(78)` — the add-on does not start. ⚠️ **And the ingress panel goes
with it**, because the add-on serving it is not running, so recovery means the
Supervisor UI. SAFETY-39's rule 10 made this reachable from a config that
started fine on the previous version: 0.7.0's own default `slew_w: 1000`
exceeds the 555.6 W ceiling on a 5000 W rail.
Run it inside the container, so the validator half actually runs:
docker run --rm -v <repo>:/w:ro goodwe-addon-checks:1 \
python3 /w/goodwe_controller/tools/predeploy_check.py \
--deployed-version 0.7.0 --options /w/live-options.json
Export `live-options.json` from the Supervisor first — the options blob is not
reachable through the Home Assistant REST API, and no entity carries it.
**Exit codes.** `0` cleared · `1` REFUSED, do not deploy · `2` NOT CLEARED,
meaning a check did not run. ⚠️ **2 is not a pass.** On a bare host without the
add-on's dependencies the validator half skips and returns 2 rather than
pretending; `test_version_gate.py` pins that a skipped check can never exit 0,
because the first draft of that test reported four silent skips as passes.
⚠️ **Why this is not a CI check.** The obvious gate — "a pull request touching
runtime code must bump the version" — is the wrong shape here. This project
deliberately batches many merges into one deploy, so a per-PR bump would put
every branch in conflict over one line and still not prevent the failure, which
happens at deploy and not at merge. The check belongs where the mistake is made.
⚠️ This file's own change does **not** need a version bump, and the gate agrees:
`tools/` and `test_*.py` are not in its runtime set, because demanding a bump
for a typo fix trains people to bump without reading — the habit the check
exists to prevent.