On this page
A WordPress plugin carries its version number in two places, and almost everybody assumes one of them is decorative. The plugin header in the main PHP file is the one you edit, the one your IDE shows you, and the one WordPress displays on the Plugins screen. The readme.txt has a field called Stable tag, which looks like documentation.
It is not documentation. On WordPress.org, Stable tag decides which tag in Subversion gets served to every user pressing update. The header version is a label. The Stable tag is the release.
=== My Plugin ===
Requires at least: 6.0
Tested up to: 6.8
Stable tag: 1.3.2 <- this is what users install
License: GPLv2 or laterHow they drift
They drift because releasing feels finished before it is. You bump the header, you write the changelog, you build the ZIP, you tag. The Stable tag sits four lines up in a file you only opened to add the changelog entry, and it keeps the number it had last time.
Nothing catches this. The plugin installs, activates and runs. The ZIP on your own site is correct. Only the users updating from the directory get the old code, and they have no reason to report it, because from where they are standing the plugin is simply not changing.
Auditing a set of plugins before publishing them, two of four had drifted — one by three minor versions. Both had been shipping the newer code by direct download and the older code by update for months.
The other field that silently fails you
Tested up to has the opposite failure. Set it below the current WordPress release and the directory shows visitors a warning that your plugin is untested. Set it above the current release — because you were being optimistic, or you typed a version that does not exist yet — and the review team rejects the submission outright. It has to name a real, released version.
The same audit found one plugin claiming a WordPress version that had not shipped, sitting alongside three that named the current one. Nothing in the build objected.
The check
Both of these are one shell command, which is the argument for running it: the cost of the check is far below the cost of the failure it prevents.
# Compare the header version against the Stable tag for every plugin
for d in plugins/*/; do
tag=$(grep -i '^Stable tag:' "$d/readme.txt" | sed 's/.*: *//')
ver=$(grep -rhiE '^ * Version:' "$d"/*.php | head -1 | sed 's/.*: *//')
[ "$tag" = "$ver" ] || echo "MISMATCH $d header=$ver stable=$tag"
doneThe rest of the handover checksThe version that ships is one line on a 49-point pass. Redirects, indexability, backups and account ownership are the others.A release process without a check is a habit, and habits drift.
