diff --git a/.claude/skills/hassio-addon-workflow/references/evidence.md b/.claude/skills/hassio-addon-workflow/references/evidence.md index 49e535beed..36406a5dbd 100644 --- a/.claude/skills/hassio-addon-workflow/references/evidence.md +++ b/.claude/skills/hassio-addon-workflow/references/evidence.md @@ -58,3 +58,64 @@ Once the rebuilt add-on is running, re-run the measurement that motivated the wo cannot be self-verified — a service that reads its environment only at start makes an env-var fix unproven until the add-on restarts, which needs the user or `ha-cli` with their agreement. If you cannot restart, the change is **Assumed**, not Verified, and must be reported that way. + +## When your change is blamed for a regression, ask for a version rollback first + +PR #3026 added an nginx `sub_filter` to the `filebrowser_quantum` ingress vhost. Right after it +merged the user reported Download had broken — the file opened inline instead of downloading — and +confirmed it still worked on the add-on's direct port, which serves `direct.conf` and carries no +`sub_filter`. That made the filter the only ingress-side change in the update, so PR #3028 reverted +it. Meanwhile every measurement said the filter was not involved: + +- the shipped bundle's Download action builds an `` and clicks it, and never calls + `window.open`, which is all the injected shim overrides; +- the shim's own predicate, evaluated live in the running app, returned false for the download + URL, the `inline=true` raw URL and the public-share download URL; +- the download response through the vhost was **byte-identical** with and without the `sub_filter` + (`Content-Disposition: attachment` intact, `Content-Length` unchanged), for `text/plain` and for + the `text/html` case the filter actually scans; +- upstream's frontend download code is unchanged from v1.5.0-stable to v1.5.4-stable; Supervisor's + ingress proxy forwards `Content-Disposition` (`_response_header` drops only `Transfer-Encoding`, + `Content-Length`, `Content-Type`, `Content-Encoding`); and the ingress panel's iframe carries no + `sandbox` attribute, so downloads are not sandbox-blocked. + +The measurements were right. The user then rolled their add-on back to **1.5.3**, which predates the +filter, saw the identical behaviour, and the revert was closed unmerged. + +**The lesson is about sequencing, not about who was right.** "Works on the old version, breaks on the +new one" is the only cheap experiment that actually isolates a shipped change, and only the reporter +can run it. Ask for it *first* — before building a reproduction, before opening a revert. It costs +them one add-on downgrade and it either confirms the regression or, as here, redirects the whole +investigation. A revert is the fallback for when they cannot roll back, not the opening move. + +Two corollaries: + +- **Do not let a clean local reproduction settle it either.** Being unable to reproduce is not + evidence of absence, and the reporter watching it fail on their own instance outranks it. Both + sides of that needed the rollback to resolve. +- **`build_from: :latest` means every merge ships an upstream version bump too**, so "it + broke when your PR landed" never implicates the diff on its own. Record which upstream version + each add-on image was built from — the registry config blob's `created` timestamp, the resolved + manifest digest, and the binary's version string. Here: add-on 1.5.3 was built 2026-08-28 23:29 + UTC from upstream v1.5.3-stable, 1.5.3.1 on 2026-08-30 06:00 UTC from v1.5.4-stable + (`sha256:e549e1a9…`). If you do use a revert as the experiment, both builds must resolve the same + upstream digest or it proves nothing: + + ```bash + T=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository::pull" | jq -r .token) + curl -s -H "Authorization: Bearer $T" \ + -H "Accept: application/vnd.oci.image.index.v1+json" \ + "https://registry-1.docker.io/v2//manifests/latest" \ + | jq -r '.manifests[] | select(.platform.architecture=="amd64" and .platform.os=="linux") | .digest' + ``` + +Worth building anyway, because it is reusable and needs no dockerd: a reproduction rig made **out of +the published image's own layers**. Pull the manifest and blobs from the registry with `curl` + `jq`, +untar them in order, then run the extracted binary through the image's own musl loader +(`root/lib/ld-musl-x86_64.so.1 ./filebrowser`) with the real `http/dist` next to it. Put the add-on's +rendered `ingress.conf` in front of it, and a second nginx in front of that to strip the +`/api/hassio_ingress/` prefix the way Supervisor does. That gets the real frontend, the real +backend and the real vhost under a browser — everything except Supervisor itself. Plain `tar` does +not interpret whiteouts, so a file a later layer deletes (`.wh.`) or a directory it marks +opaque (`.wh..wh..opq`) survives into the reconstructed rootfs; check with +`tar tzf | grep '\.wh\.'` and reach for an OCI-aware unpacker if any turn up.