diff --git a/docs/REUSE.md b/docs/REUSE.md index f8dbb79..280dc29 100644 --- a/docs/REUSE.md +++ b/docs/REUSE.md @@ -72,6 +72,8 @@ it cannot. that run is green. - `release.yml` — tag, test, build, publish, then read the feed back with no credentials to prove a shop can download it. +- `ci/update-smoke.sh` — install the previous version into a throwaway shop and + let WordPress update it by itself. The only check that proves the whole chain. - `ci/workflows-in-sync.sh` — fails when the two workflow roots drift, which is the only way to notice that a workflow stopped running on Gitea. @@ -88,6 +90,13 @@ and a release with no zip attached both produce a page that looks perfect to anyone logged in, and an update that no shop is ever offered. A check that sends a token is the false positive that hides exactly that. +**A forge's own download URL is not the address your users can reach.** +`browser_download_url` is built from the forge's configured root URL; +`gitea.nolimit.dk` reports `http://57.129.89.117:25001/...` there. The updater +rebuilds the address from the `Update URI` header and keeps only the path. This +one hides particularly well: a CI job on the same network reaches that raw IP, +so following the reported URL goes green and only a real client outside fails. + ## Candidates already in the fleet `spool` and `subscriptions` are both WooCommerce plugins with some of this diff --git a/docs/TESTING-AND-RELEASES.md b/docs/TESTING-AND-RELEASES.md index fee13c4..aef789b 100644 --- a/docs/TESTING-AND-RELEASES.md +++ b/docs/TESTING-AND-RELEASES.md @@ -16,6 +16,8 @@ Three questions, answered by three things in this repository. ci/static.sh # syntax, hygiene, versions, headers — seconds ci/site-test.sh # a real WordPress + WooCommerce — a few minutes ci/build-zip.sh # what a shop actually installs +FROM_VERSION=1.4.0 \ + ci/update-smoke.sh # let WordPress update itself from the live feed ``` `ci/site-test.sh` takes three knobs: @@ -110,6 +112,31 @@ Two things in there are guards against bugs that are invisible without them: --- +## Proving an update actually arrives + +`ci/update-smoke.sh` installs the **previous** version into a throwaway shop +and then gets out of the way: WordPress's own update check finds the new one, +downloads it and installs it. + +```sh +FROM_VERSION=1.4.0 ci/update-smoke.sh +``` + +Nothing else can stand in for it. Every piece of the chain can be individually +correct and the update still not arrive: + +- a package URL on an address the shop cannot reach, +- a zip whose top-level directory is not the plugin slug — which installs a + *second* copy beside the first and leaves the old one active, +- a version WordPress does not consider newer than what is installed. + +It checks all three, plus that the plugin is still active afterwards and that +WordPress stops offering the update once it is applied. + +Run it after a release. + +--- + ## How the automatic updates work The plugin header carries: @@ -149,6 +176,28 @@ If you would rather keep the source private, create a separate public repo that holds only releases and point `Update URI` at that instead — the updater derives everything else from the header, so that is a one-line change. +### The download address is rebuilt, not taken + +The updater ignores the `browser_download_url` the API hands back and builds +the address from the `Update URI` header instead. + +That field is filled from the forge's own configured root URL, which is not +necessarily the address the outside world reaches it on. `gitea.nolimit.dk` +returns `http://57.129.89.117:25001/...` there — plain HTTP, a raw IP, a +non-standard port, and around the Cloudflare tunnel in front of it. A shop +handed that would be downloading plugin code over an unencrypted connection to +an address nothing guarantees will keep working. + +The paths Gitea reports are correct; only the host is wrong. So the host comes +from the header — the one place that already had to be right for any of this +to work — and the path is kept. + +This is worth knowing because of *how* it hides: a CI job on the same network +reaches that raw IP perfectly well, so a verification step that follows the +reported URL goes green. Only a real client outside fails. The release check +therefore downloads the address **the plugin builds**, and opens the zip to +confirm it is the plugin rather than an error page with a 200 on it. + ### It reports nothing The update check sends a plain unauthenticated GET for a file every visitor to