Three gaps found by being asked plain questions about it.
**The auto-update toggle was a promise nobody had checked.** offer_update()
answers even when the installed version is current, on the reasoning that the
answer has to land in WordPress's no_update list for the "Enable auto-updates"
link to exist at all. That was a comment and an intention; it is now an
assertion against the real WP_Plugins_List_Table, which is where the flag is
actually read:
$plugin_data = array_merge( (array) $plugin_info->no_update[ $plugin_file ],
array( 'update-supported' => true ), $plugin_data );
A plugin in neither response nor no_update gets "Auto-updates are not available
for this plugin" while looking entirely healthy.
**An icon.** Without one, Dashboard → Updates shows a generic grey plug.
assets/icon.svg, handed over in the offer's icons key, served from the shop's
own copy rather than the release host — one fewer thing that must be reachable
for wp-admin to render, and it cannot go mixed content on an HTTPS shop. Drawn
as paths rather than <text>: an SVG that leans on a font renders differently on
every machine, and this one is shown at about 40px, which is the size where
that is the difference between a mark and a smudge.
**"Check again" now checks again.** The six-hour cache is right for the check
WordPress runs twice a day and wrong for a link whose whole promise is that it
looks now. Publishing a release and then clicking it showed nothing for up to
six hours, which reads exactly like a broken updater.
And docs/REUSE.md was missing all of this: it covered Update URI, where to
instantiate the updater and making the repo public, but not the one rule that
is genuinely easy to get wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
135 lines
6.6 KiB
Markdown
135 lines
6.6 KiB
Markdown
# Using this setup in another plugin
|
||
|
||
Everything here was built to be lifted into the next WordPress plugin. Nothing
|
||
under `ci/` knows this plugin's name except `ci/plugin.conf`.
|
||
|
||
There is no shared repository and no reusable workflow to `uses:`. That is
|
||
deliberate: these repositories live on two forges, and a cross-forge
|
||
`workflow_call` resolves on GitHub and fails on Gitea. Copying six files and
|
||
editing one is a smaller cost than a dependency that works in half the places
|
||
it runs.
|
||
|
||
## The port
|
||
|
||
**1. Copy these, unchanged:**
|
||
|
||
```
|
||
ci/ every script; plugin.conf is the only one you edit
|
||
.github/workflows/ ci.yml, compat.yml, release.yml, auto-merge.yml
|
||
.gitea/workflows/ ci.yml, compat.yml, release.yml, README.md
|
||
.distignore
|
||
```
|
||
|
||
**2. Rewrite `ci/plugin.conf`** — five values, all commented in the file:
|
||
|
||
```sh
|
||
PLUGIN_SLUG=your-plugin
|
||
PLUGIN_MAIN=your-plugin.php
|
||
TEXT_DOMAIN=your-plugin
|
||
NEEDS_WOOCOMMERCE=yes # no, for a plugin that does not need it
|
||
SITE_TESTS="tests/fixtures.php tests/smoke.php"
|
||
```
|
||
|
||
**3. Adjust `.distignore`** for that plugin's internal documents. Everything
|
||
left in `wp-content/plugins/` can in principle be fetched over HTTP from a live
|
||
shop, so notes and tooling stay out of the zip.
|
||
|
||
**4. Write `tests/fixtures.php` and `tests/smoke.php`.** These are the only
|
||
genuinely new work. Use this plugin's as the shape: `wc_edm_ok()` /
|
||
`wc_edm_is()` accumulate failures so one run reports all of them, and the file
|
||
exits non-zero at the end. They run under `wp eval-file`, so WordPress and
|
||
WooCommerce are fully loaded and every API is the real one.
|
||
|
||
**5. Copy `includes/class-updater.php`** and rename the class, the `TRANSIENT`
|
||
constant, the plugin-file constant and the user-agent string. Then add to the
|
||
plugin header:
|
||
|
||
```
|
||
Update URI: https://gitea.nolimit.dk/kw/your-plugin
|
||
```
|
||
|
||
and instantiate the updater **before** any "is WooCommerce active" gate — a
|
||
shop with WooCommerce switched off for an afternoon must not quietly stop
|
||
receiving fixes.
|
||
|
||
Three things in that file are load-bearing and none of them look it:
|
||
|
||
- 🔴 **`offer_update()` answers even when the installed version is already
|
||
current.** It is tempting to return `false` there — there is no update, after
|
||
all — and that one line is what silently removes the **Enable auto-updates**
|
||
link from the Plugins screen. `WP_Plugins_List_Table` sets
|
||
`update-supported` only for plugins it finds in the update transient's
|
||
`response` **or** `no_update` list; a plugin that stays silent is in neither,
|
||
and wp-admin says "Auto-updates are not available for this plugin." The
|
||
plugin looks perfectly configured and can never auto-update.
|
||
`tests/smoke.php` asserts the rendered table, not the intention.
|
||
- **Set `icons`.** Without it Dashboard → Updates shows a generic grey plug.
|
||
Point it at the shop's own copy (`PLUGIN_URL . 'assets/icon.svg'`), not the
|
||
release host: one fewer thing that must be reachable for wp-admin to render,
|
||
and it cannot go mixed-content on an HTTPS shop.
|
||
- **`Update URI` also switches wordpress.org off** for this plugin, from
|
||
WordPress 5.8. Remove the header and a .org plugin that happens to share the
|
||
slug could be pushed onto a shop running yours.
|
||
|
||
The answer is cached for six hours, which is right for the check WordPress runs
|
||
twice a day and wrong for the **Check again** link — so `forget_on_force_check()`
|
||
drops the cache on `force-check`. Without it, someone who has just published a
|
||
release clicks that link, sees nothing, and concludes the whole mechanism is
|
||
broken.
|
||
|
||
**6. Make the Gitea repository public**, or point `Update URI` at a separate
|
||
public releases-only repository. Every shop reads that feed without
|
||
credentials; `ci/publish-release.sh` fails the release with this instruction if
|
||
it cannot.
|
||
|
||
## What you get
|
||
|
||
- `ci/static.sh` — syntax on the oldest and newest PHP, the direct-access
|
||
guard on every shipped file, no debug leftovers, no short tags, text domain
|
||
on every translation call, one version stated once across header, constant
|
||
and readme, and a warning when the compatibility headers have gone stale.
|
||
- `ci/site-test.sh` — a real WordPress + WooCommerce at any versions you name,
|
||
including `nightly` and `beta`, failing on any deprecation from your own code.
|
||
- `ci/build-zip.sh` — an installable zip, checked for the main file, for a
|
||
single top-level directory named for the slug, and for leaked tooling.
|
||
- `compat.yml` — the weekly run against the next WordPress and the next
|
||
WooCommerce, and a pull request that bumps the compatibility headers once
|
||
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.
|
||
- Assertions that the **Enable auto-updates** link is actually offered, built
|
||
from the real `WP_Plugins_List_Table`, and that the update offer carries an
|
||
icon that exists in the release.
|
||
- `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.
|
||
|
||
## Two guards worth understanding before you move them
|
||
|
||
**`.gitea/workflows/` hides `.github/workflows/` completely.** Gitea uses the
|
||
former if it exists and never merges the two. Adding one Gitea-only workflow
|
||
switches off CI for the whole repository there, with no error and no run to
|
||
click on — it looks exactly like a runner that has not picked the job up. Every
|
||
workflow that must run on Gitea has to be in that directory.
|
||
|
||
**The release reads its own feed anonymously afterwards.** A private repository
|
||
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
|
||
already: `spool` has an earlier version of the throwaway-site script, and
|
||
`subscriptions` has the hygiene checks and a `.distignore`. Neither has the
|
||
version matrix, the compatibility schedule or the updater. Porting them is
|
||
steps 1–5 above.
|