|
|
|
|
@@ -59,13 +59,11 @@ cannot be self-verified — a service that reads its environment only at start m
|
|
|
|
|
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
|
|
|
|
|
## A local reproduction that passes does not outrank a user's production report
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
PR #3026 added an nginx `sub_filter` to the `filebrowser_quantum` ingress vhost. After it merged,
|
|
|
|
|
the user reported that Download had stopped working — the file opened inline instead. Every check
|
|
|
|
|
run against the change came back clean:
|
|
|
|
|
|
|
|
|
|
- the shipped bundle's Download action builds an `<a href>` and clicks it, and never calls
|
|
|
|
|
`window.open`, which is all the injected shim overrides;
|
|
|
|
|
@@ -74,48 +72,46 @@ it. Meanwhile every measurement said the filter was not involved:
|
|
|
|
|
- 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.
|
|
|
|
|
- upstream's frontend download code is unchanged from v1.5.0-stable to v1.5.4-stable, and Home
|
|
|
|
|
Assistant's ingress iframe carries no `sandbox` attribute (`ha-panel-app.ts`), so downloads are
|
|
|
|
|
not sandbox-blocked either.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
None of that reproduced the failure, and none of it explained the one fact that mattered: the user
|
|
|
|
|
saw it fail under ingress and work on the add-on's direct port, and the `sub_filter` was the only
|
|
|
|
|
ingress-side change in that update. **Revert first and keep investigating.**
|
|
|
|
|
|
|
|
|
|
**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.
|
|
|
|
|
A revert can also discriminate — if the symptom survives it, the cause was the concurrent upstream
|
|
|
|
|
bump rather than the diff — but only if **both builds resolve the same upstream image**. With
|
|
|
|
|
`build_from: <image>:latest` they need not: `latest` can move between the two rebuilds, and then
|
|
|
|
|
the comparison proves nothing. Resolve the tag to a digest and record it before and after, or pin
|
|
|
|
|
`build_from` to that digest for the duration of the experiment:
|
|
|
|
|
|
|
|
|
|
Two corollaries:
|
|
|
|
|
```bash
|
|
|
|
|
T=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:<repo>: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/<repo>/manifests/latest" \
|
|
|
|
|
| jq -r '.manifests[] | select(.platform.architecture=="amd64" and .platform.os=="linux") | .digest'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- **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: <image>: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:
|
|
|
|
|
For the case above both builds resolved `gtstef/filebrowser:latest` amd64 to
|
|
|
|
|
`sha256:e549e1a9b5de573c276f7be67444841559e3f89cbc664a2f926968453d069436` (v1.5.4-stable, created
|
|
|
|
|
2026-08-29T14:19:56Z), so that comparison was controlled — but it was luck, not design.
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
T=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:<repo>: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/<repo>/manifests/latest" \
|
|
|
|
|
| jq -r '.manifests[] | select(.platform.architecture=="amd64" and .platform.os=="linux") | .digest'
|
|
|
|
|
```
|
|
|
|
|
Worth building next time, because it took most of the session and is reusable: a reproduction rig
|
|
|
|
|
made **out of the published image's own layers**, with no dockerd. 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. Plain `tar` does not interpret whiteouts, so a file a later layer deletes
|
|
|
|
|
(`.wh.<name>`) or a directory it marks opaque (`.wh..wh..opq`) survives into the reconstructed
|
|
|
|
|
rootfs; check with `tar tzf <layer> | grep '\.wh\.'` and reach for an OCI-aware unpacker if any
|
|
|
|
|
turn up. (There were none in `gtstef/filebrowser:latest` — all eleven layers, 0 whiteout entries —
|
|
|
|
|
so that rig was faithful.) Put the add-on's rendered `ingress.conf` in front of it, and a second nginx server in
|
|
|
|
|
front of that to strip the `/api/hassio_ingress/<token>` prefix the way Supervisor does. That gets
|
|
|
|
|
the real frontend, the real backend and the real vhost under a browser — everything except
|
|
|
|
|
Supervisor itself.
|
|
|
|
|
|
|
|
|
|
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/<token>` 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.<name>`) or a directory it marks
|
|
|
|
|
opaque (`.wh..wh..opq`) survives into the reconstructed rootfs; check with
|
|
|
|
|
`tar tzf <layer> | grep '\.wh\.'` and reach for an OCI-aware unpacker if any turn up.
|
|
|
|
|
**`build_from: <image>:latest` means every merge ships an upstream version bump too.** 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 — before attributing a regression to
|
|
|
|
|
the diff.
|
|
|
|
|
|