Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
208 lines
8.8 KiB
Markdown
208 lines
8.8 KiB
Markdown
# Testing, releases and automatic updates
|
|
|
|
Three questions, answered by three things in this repository.
|
|
|
|
| Question | Answer |
|
|
| --- | --- |
|
|
| Does it still work on the newest WordPress and WooCommerce? | `ci/site-test.sh`, run weekly by `compat.yml` against `latest`, `nightly` and `beta` |
|
|
| Can updates be pushed out automatically? | Yes — `includes/class-updater.php` plus `release.yml` |
|
|
| What happens when *I* press update in wp-admin? | Nothing surprising, because the release was tested against those versions before it was published |
|
|
|
|
---
|
|
|
|
## Running the tests
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
WP_VERSION=latest # or nightly, or an exact version like 7.0
|
|
WC_VERSION=latest # or beta, or an exact version like 11.1.0
|
|
PHP_VERSION=8.3 # any tag of the wordpress:cli image
|
|
```
|
|
|
|
It builds a throwaway site in Docker containers, installs WordPress and
|
|
WooCommerce at those versions, copies in **exactly the files the release zip
|
|
will contain**, activates the plugin, and runs `tests/fixtures.php` and
|
|
`tests/smoke.php` inside it with `wp eval-file`.
|
|
|
|
The tests use a real cart, real coupons and real `WC_Shipping_Rate` objects.
|
|
That is the point. A mocked cart keeps passing after WooCommerce has changed
|
|
underneath it, which is the one failure this whole directory exists to catch.
|
|
|
|
Everything is torn down afterwards — containers, volumes and the network — so
|
|
running it twice leaves nothing behind.
|
|
|
|
### Deprecations count as failures
|
|
|
|
The site test switches on `WP_DEBUG` and fails the build on any notice logged
|
|
from this plugin's directory. That is how a WordPress or PHP upgrade actually
|
|
breaks a plugin: nothing fails, a notice appears, and two releases later it is
|
|
a fatal. Notices from WordPress core and WooCommerce are ignored — failing on
|
|
those would make the build red for something nobody here can fix, and a build
|
|
that is always red is a build that gets ignored.
|
|
|
|
---
|
|
|
|
## The weekly compatibility run
|
|
|
|
`compat.yml` runs the matrix every Monday at 06:00 and on demand:
|
|
|
|
| WordPress | WooCommerce | PHP | What it tells you |
|
|
| --- | --- | --- | --- |
|
|
| latest | latest | 8.3 | today's shop |
|
|
| **nightly** | latest | 8.3 | the next WordPress, weeks early |
|
|
| latest | **beta** | 8.3 | the next WooCommerce, weeks early |
|
|
| latest | latest | 7.4 | the floor the plugin header promises |
|
|
| latest | latest | 8.4 | where deprecations appear first |
|
|
|
|
The `beta` cell falls back to the stable release when there is no open beta, so
|
|
it is never silently skipped.
|
|
|
|
When the whole matrix passes, a second job compares `Tested up to:` and
|
|
`WC tested up to:` against the current releases and opens a pull request if
|
|
they are behind. It runs only after the matrix is green, because those headers
|
|
are a claim — WooCommerce shows `WC tested up to` to the shop owner as an
|
|
untested-plugin banner, and that banner is what they read before deciding
|
|
whether to press update.
|
|
|
|
The schedule fires on both forges; the matrix job skips the scheduled run on
|
|
Gitea so the single self-hosted runner is not asked to repeat GitHub's work.
|
|
`workflow_dispatch` still works there.
|
|
|
|
---
|
|
|
|
## Cutting a release
|
|
|
|
1. Bump `Version:` in the plugin header. `ci/static.sh` checks that
|
|
`WC_EDM_VERSION` and the readme's `Stable tag:` agree with it — WordPress
|
|
compares the update feed against the header and nothing else, so a
|
|
disagreement means the update is offered again immediately after it is
|
|
installed, forever.
|
|
2. Add a changelog section to `README.txt`.
|
|
3. Merge to `main`.
|
|
4. Tag it **annotated**, because the message is the release notes:
|
|
```sh
|
|
git tag -a v1.4.0 -m "..."
|
|
git push origin v1.4.0 # origin pushes to GitHub and Gitea both
|
|
```
|
|
|
|
`release.yml` then runs the static checks, installs the plugin into a real
|
|
WordPress + WooCommerce and runs the full test suite, builds the zip, publishes
|
|
the release, and finally **reads the feed back anonymously** to prove a shop
|
|
can actually download it.
|
|
|
|
Two things in there are guards against bugs that are invisible without them:
|
|
|
|
- **The tag message is re-fetched before it is read.** `actions/checkout`
|
|
fetches every tag correctly and then overwrites the triggering one with the
|
|
commit SHA, turning an annotated tag into a lightweight one. Reading it then
|
|
returns the *commit* message — on a merge commit, "Merge pull request #12
|
|
from …". Nothing fails; the release is just published with the wrong text.
|
|
- **The anonymous read-back sends no credentials.** The two ways the feed
|
|
breaks — a private repository and a release with no zip attached — both
|
|
leave a release page that looks perfectly fine to anyone logged in.
|
|
|
|
---
|
|
|
|
## 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:
|
|
|
|
```
|
|
Update URI: https://gitea.nolimit.dk/kw/woocommerce-email-discount-manager
|
|
```
|
|
|
|
`includes/class-updater.php` hooks `update_plugins_gitea.nolimit.dk`, the
|
|
filter WordPress fires for plugins whose `Update URI` points at that host. It
|
|
reads `/api/v1/repos/kw/<repo>/releases/latest`, takes the tag as the version
|
|
and the attached `.zip` as the package, and hands both to WordPress. WordPress
|
|
does the version comparison, the download, the unpacking and the rollback.
|
|
|
|
That means all of this works with no extra code:
|
|
|
|
- the update notice on **Plugins** and **Dashboard → Updates**
|
|
- the **update now** link
|
|
- the per-plugin **Enable auto-updates** link — WordPress builds that from the
|
|
plugins it has an answer for, which is why the updater answers even when the
|
|
installed version is already current
|
|
- `wp plugin update woocommerce-email-discount-manager` over WP-CLI
|
|
|
|
`Update URI` earns its place a second way: since WordPress 5.8, a plugin that
|
|
declares one is **never asked about on wordpress.org**. Without it, a .org
|
|
plugin that happened to share this slug could be pushed onto a shop running
|
|
this one.
|
|
|
|
### One-time setup
|
|
|
|
The repository on Gitea must be **public**, because every shop reads that feed
|
|
without credentials. Settings → make it public. Nothing in a GPL plugin zip is
|
|
secret, and the release workflow fails loudly with this exact instruction if it
|
|
is not done.
|
|
|
|
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
|
|
that repository can read. The user agent is overridden on purpose: WordPress's
|
|
default one is `WordPress/7.1; https://the-shop.example`, which would tell the
|
|
update host which shops are running this plugin. Nothing else about the site
|
|
leaves it, ever.
|