# 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 ``` `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. --- ## 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//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. ### 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.