Skip to content

Releases: csmcneill/update-doctor

Update Doctor 1.2.11

Choose a tag to compare

@csmcneill csmcneill released this 17 Jul 01:00

Makes the update transient's package field — the thing that decides whether a cleared update can actually download — visible in the report, with no WP-CLI access required.

New: package classification in Per-Plugin and Per-Theme Decisions

Previously, any non-empty package produced the same line: "would auto-update on next cron run." But non-empty covers three very different fates:

  • A real download URL → now shown as would auto-update on next cron run (package: woocommerce.com) — hostname only, because signed download URLs can embed auth tokens and these reports get pasted into tickets.
  • WooCommerce.com's woocommerce-com-expired-* renewal marker → now named explicitly: core will block this download with a renewal notice, so calling it "would auto-update" was wrong.
  • Any other non-URL junk → quoted (truncated) and flagged as undownloadable.

Empty packages keep the existing license-gated wording, and the license-gated summary now counts markers and malformed values too.

This closes the report's last blind spot for diagnosing marketplace-delivered (WooCommerce.com / Freemius / EDD) update entitlement — you can now see empty vs marker vs real-URL directly in the per-item list.

Upgrading

Replace the plugin folder with update-doctor-1.2.11.zip (or upload via Plugins → Add New → Upload Plugin and overwrite). On hosts where the OPcache check warns (validate_timestamps = 0 — Pressable and WordPress.com Atomic both qualify), clear the host cache after updating or the new code won't run.

Update Doctor 1.2.10

Choose a tag to compare

@csmcneill csmcneill released this 16 Jul 22:40

Makes the failure monitor tell the truth — and tell you what it found.

Fix: download failures were invisible

When an update's download fails, WordPress records the item's result as null, not false or a WP_Error — the upgrader returns before a result is ever set. Both the email monitor and the activity log's batch counts only treated false/WP_Error as failures, so a batch where six downloads failed reported "0 failed." Success is now exactly true; anything else counts as a failure (which is core's own convention).

Fix: the license-gate test was too weak

The "can this item actually auto-update?" test accepted any non-empty package value. Some marketplaces inject non-empty marker strings (e.g. WooCommerce.com's woocommerce-com-expired-…) that can never download. The test now requires a real http(s):// URL and is applied on the failure path as well as the silent-skip path — so license-gated items stay excluded from alerts no matter how a marketplace changes what it writes into the update transient between versions.

New: alerts name the items

The email now lists each failure with WordPress's reason, and each stuck update with its pending version and how long it has waited — instead of a generic "an automatic update issue was detected." You can finally tell a real problem from noise without opening the diagnostic page.

New: per-item alert memory

A persistently-stuck item alerts at most once per week, instead of every 24 hours forever. New problems still alert immediately. The 24-hour global throttle remains.

Fix: the activity log no longer over-claims "upgraded"

Bulk upgrade completions fire once listing every requested item regardless of outcome — they're now recorded as bulk_upgrade_process with no success claim. Single completions check the upgrader's actual result and record upgrade_failed when the install didn't succeed. Successful single plugin upgrades also record the version now on disk, which exposes updates that apply and then don't stick (install failures or post-update rollbacks).

Upgrading

Replace the plugin folder with update-doctor-1.2.10.zip (or upload via Plugins → Add New → Upload Plugin and overwrite). On hosts where the OPcache check warns (validate_timestamps = 0, e.g. Pressable), clear your host's cache after updating or the new code won't run.

Update Doctor 1.2.9

Choose a tag to compare

@csmcneill csmcneill released this 07 Jul 00:19

Stops recurring false "automatic update issue detected" emails for license-gated premium plugins.

The problem

The email failure monitor alerts on failures or "silent skips" — plugins that were opted into auto-updates and had a pending update but didn't apply within six hours. The snapshot that built that list tracked any opted-in, pending plugin without checking whether it actually had a download package.

A premium plugin (WooCommerce.com, Freemius, EDD) whose subscription has lapsed shows a pending update but ships no package — WordPress can never auto-update it. So it aged past the grace window and tripped a silent-skip email every 24 hours, indefinitely, even though nothing was actually broken. On a site with several lapsed premium subscriptions, that's a permanent stream of false alarms.

The fix

The monitor now only tracks and alerts on items that can actually auto-update:

  • Snapshot guards — the expected-update list skips plugins and themes whose transient entry has no package URL.
  • Silent-skip recheck — before sending an alert, it re-verifies the item still has a pending, downloadable package right now. This also flushes license-gated entries recorded by earlier versions, so the false emails stop on the next scheduled run rather than after one more.
  • Genuine silent skips still alert — an item that genuinely had a package and still didn't apply is a real problem, and is reported exactly as before.

Upgrading

Replace the plugin folder with update-doctor-1.2.9.zip (or upload via Plugins → Add New → Upload Plugin and overwrite). On hosts where the OPcache check warns (validate_timestamps = 0), clear your host's cache after updating or the new code won't run.

Update Doctor 1.2.8

Choose a tag to compare

@csmcneill csmcneill released this 26 Jun 17:17

Stops the diagnostic from reporting false failures when it's opened during a real, in-progress automatic update.

The problem

If you run Update Doctor at the exact moment the host's scheduled updater is mid-run, the auto_updater.lock is legitimately held. Two checks misread that healthy state as a failure:

  • The create_lock() probe returned [ISSUE] the updater cannot acquire its lock — because a real update was holding it.
  • The Last Update Attempt check returned [ISSUE] exited because the auto_updater.lock was already held — because a manual test correctly stood down rather than run a second updater in parallel.

Both contradicted the Options check sitting right above them, which already said (correctly) "An auto-update is currently running."

The fix

Both checks now cross-reference the shared lock-state detector. A fresh, unmasked lock held in both the database and the object cache means an update is genuinely running right now, so they report [INFO] — "an update is currently in progress" / "test deferred — an update was already in progress" — instead of a failure. A stale or cache-masked lock still reports a real stuck-lock [ISSUE], exactly as before.

In other words: the lock doing its job is no longer mistaken for the lock being broken.

Upgrading

Replace the plugin folder with update-doctor-1.2.8.zip (or upload via Plugins → Add New → Upload Plugin and overwrite). On hosts where the OPcache check warns (validate_timestamps = 0), clear your host's cache after updating or the new code won't run.

Update Doctor 1.2.7

Choose a tag to compare

@csmcneill csmcneill released this 25 Jun 02:04

Two small correctness fixes surfaced while validating 1.2.6.

Fix: single-theme upgrades logged as "theme: (unknown)"

The Auto-Update Activity Log recorded a scheduled single-theme upgrade with no name. The recorder read the bulk themes key from upgrader_process_complete but not the singular theme key that a single-theme auto-update sets — even though the plugin side already handled both plugins and plugin. Theme names now record correctly.

Fix: stale "Run Background Update Now" button name

That was the plugin's original button label, long since renamed. It lingered in the Last Update Attempt and Cron sections; it now reads "Manual Update Test", matching the actual button. (Generic "background updates" wording, where it refers to the concept rather than the button, is unchanged.)

Upgrading

Replace the plugin folder with update-doctor-1.2.7.zip (or upload via Plugins → Add New → Upload Plugin and overwrite). On hosts where the OPcache check warns (validate_timestamps = 0, e.g. Pressable), clear your host's cache after updating or the new code won't run.

Update Doctor 1.2.6

Choose a tag to compare

@csmcneill csmcneill released this 25 Jun 01:34

Sharper, more honest UI — and a new check born from a real debugging detour.

New: PHP OPcache check

Surfaces opcache.enable, validate_timestamps, revalidate_freq, and restrict_api, and warns when OPcache does not validate file timestamps. That's the state where updated plugin, theme, and core files keep running their previously-compiled bytecode until the host's cache is cleared — so an update installs successfully yet appears to do nothing. It reads the directives via ini_get(), so it works even on hosts (like Pressable) where restrict_api locks the opcache_*() API and opcache_get_configuration() would return nothing.

This does not flag a blocked update — it explains why new code might not run after one.

Removed: "Run Unattended Test"

The unattended button emulated a cron run inside a web request (wp_doing_cron forced true). But a web request is still a web request — it can't reproduce a real CLI/platform scheduled run, so the button gave a false sense of testing the thing that actually matters. It's gone. The honest two-path model remains:

  • Manual Update Test — a fast web-context check.
  • Run via WP-CLI — the updater in a separate command-line process, the way hosts actually schedule it.

...with the Auto-Update Activity Log recording the host's own real [scheduled] runs as ground truth.

Renamed: "Run Update Test" → "Manual Update Test"

Pairs cleanly with the [manual] tag in the activity log and contrasts with the WP-CLI path.

Changed: "Clear Stuck Update Lock" is now conditional

It appears only when Update Doctor actually detects a stuck lock — one that's stale (older than WordPress's one-hour lock TTL) or cache-masked (a database row hidden from the object cache). A destructive, irreversible delete should never be a standing button that could yank a lock a legitimate in-progress update is holding. The report's verdict and the button now share one detector, so they always agree.

Upgrading (note for OPcache hosts)

Replace the plugin folder with update-doctor-1.2.6.zip (or upload via Plugins → Add New → Upload Plugin and overwrite). On hosts where the new OPcache check warns (validate_timestamps = 0, e.g. Pressable), clear your host's cache after updating or the new code won't run — the very thing the check now tells you.

Update Doctor 1.2.5

Choose a tag to compare

@csmcneill csmcneill released this 24 Jun 22:22

Two fixes that make the Auto-Update Activity Log trustworthy. Both were caught by an unattended-test run that produced a [manual]-tagged emulated run and phantom [scheduled] lock events on a healthy site.

The activity log was recording its own diagnostics

The create_lock() probe acquires and releases auto_updater.lock on every page render to test whether the updater can grab the lock. The passive recorder was catching that release and logging it as [scheduled] lock_released — fake evidence that the host's scheduled runner had fired, when nothing of the sort happened. It was also enough to trip the "Scheduled runner touches the lock but no item attempts seen" warning. The recorder now suppresses its own probe-induced lock churn, so [scheduled] entries mean what they say.

Run-context tags could leak between requests

[manual] / [emulated] are tracked in static properties, and PHP-FPM reuses a worker process across many requests. If an earlier run died before its reset — a slow update hitting max_execution_time is an uncatchable timeout that escapes the try/catch — the stale flag survived into the next request that worker served and mislabeled it. That's exactly what happened here: an emulated run came back tagged [manual]. The dangerous version of this bug is a real scheduled run inheriting a leaked flag and being logged as [manual], hiding the genuine platform activity this plugin exists to detect.

The flags are now cleared at the start of every run and on shutdown (which fires even on a fatal), so a death mid-run can't leak forward.

Also

  • The Last Update Attempt section now names the "unattended emulation" run kind explicitly instead of calling it "automatic update run".

Upgrading

Replace the plugin folder with update-doctor-1.2.5.zip (or upload via Plugins → Add New → Upload Plugin and overwrite). Hard-refresh the diagnostic page afterward. Note that the existing log on a site already running 1.2.x may still contain a few phantom [scheduled] lock_released entries from the old probe behavior; those will roll off as new activity accumulates.

Update Doctor 1.2.4

Choose a tag to compare

@csmcneill csmcneill released this 24 Jun 20:50

Three ways to exercise the updater — honestly labeled by how close each gets to a real scheduled run.

Run Update Test (renamed from "Run Live Update Test")

The same on-demand test as before, reframed honestly: it runs WordPress's auto-updater in this web request. Useful and fast, but a web request is not how most hosts schedule unattended updates.

Run Unattended Test (new)

Runs the updater with wp_doing_cron() forced true, to mimic a scheduled run as closely as a web request can. Its activity is tagged [emulated], never [scheduled], so it can't be mistaken for a real platform run in the Activity Log. Comparing it against the plain web test is itself diagnostic — if they behave differently, request-context gating is in play.

Run via WP-CLI (new)

A modal hands you the exact command to run the updater in a separate command-line process — the closest approximation to how hosts actually schedule unattended updates, and the one path a web-request plugin can't reproduce itself:

wp eval 'require_once ABSPATH . "wp-admin/includes/class-wp-upgrader.php"; require_once ABSPATH . "wp-admin/includes/class-wp-automatic-updater.php"; ( new WP_Automatic_Updater() )->run();'

One-click copy; run it over SSH or your host's WP-CLI console. The Auto-Update Activity Log records the result.

Why three, not one

A plugin runs inside a web request and can't fully reproduce a CLI/platform process context — which is exactly the kind of difference that makes auto-updates behave differently between contexts. So rather than pretend one button is "the" test, this exposes the spectrum and lets the Activity Log (which records the host's own real scheduled runs) be the source of truth.

Upgrading

Replace the existing plugin folder with update-doctor-1.2.4.zip, or upload via Plugins → Add New → Upload Plugin and overwrite. Hard-refresh the page so the updated stylesheet and script load.

Update Doctor 1.2.3

Choose a tag to compare

@csmcneill csmcneill released this 24 Jun 20:26

Observes the host's scheduled update runs — not just the manual test — and inspects the query filter.

Auto-Update Activity Log

Every release so far measured Update Doctor's own manual trigger. This adds a passive recorder, registered on every request including WP-Cron, that captures real auto-update events as they happen: item attempts (pre_auto_update), completed upgrades, batch results, and auto_updater.lock acquire/release. Each event is tagged [scheduled] (the host's own runner) or [manual] (the Run Live Update Test button).

The new Auto-Update Activity Log check displays the timeline and reads the situation:

  • [scheduled] attempts or upgrades appear → the platform's updater is reaching WordPress's auto-updater past the lock, so any remaining stalls are per-item (opt-in, license, a per-item filter), not a global lock failure.
  • only [manual] events ever appear → the scheduled updater is not invoking the auto-updater on this site at all, which would be the real problem rather than anything the manual test triggers.

This is how to tell whether a manual-trigger stall is even meaningful: if the platform updates fine on its own schedule, the manual collision is benign.

query filter inspection

The Options and Transients section now lists any callbacks on WordPress's query filter, with file and line. Code hooked there can rewrite or reject SQL — including create_lock()'s INSERT into wp_options — so this surfaces a candidate cause for a lock write that fails with no visible lock row.

Upgrading

Replace the existing plugin folder with update-doctor-1.2.3.zip, or upload via Plugins → Add New → Upload Plugin and overwrite. The activity log fills in over time; check it after the next scheduled update window.

Update Doctor 1.2.2

Choose a tag to compare

@csmcneill csmcneill released this 24 Jun 19:47

Measures the update lock directly instead of inferring it — and tidies the button row.

create_lock() probe

The earlier "stuck lock" theory assumed a stale row in the database. A direct query disproved that (no row), yet the updater still bails at the lock. So this release stops reasoning about WP_Upgrader::create_lock() and just runs it. The auto_updater.lock check now:

  • Calls WP_Upgrader::create_lock('auto_updater') directly — the exact call that gates WP_Automatic_Updater::run() — and reports whether it succeeds, releasing the lock immediately if it acquires.
  • Runs a raw INSERT IGNORE write test against wp_options and captures $wpdb->last_error, to see whether the lock's write mechanism works at all on this site.
  • Dumps the real object-cache state (wp_cache_get, the notoptions mask) next to get_option().

The result reads the cause directly off the next report:

  • Raw write test errors → something is rejecting the lock INSERT (a query filter from a security/host plugin, or a database write restriction) — a persistent cause that travels with a migration.
  • Write ok but create_lock returns false → another process is holding the lock at that instant (contention).
  • create_lock returns true on demand → the run-time failure is a timing collision, not a stuck state.

Button alignment

The action row mixed a hero-sized primary button with a borderless link-style delete button under default flex stretch, so heights and baselines were inconsistent. Every action is now a consistent bordered button, vertically centered; the Clear Stuck Update Lock button keeps red text to mark it destructive.

Upgrading

Replace the existing plugin folder with update-doctor-1.2.2.zip, or upload via Plugins → Add New → Upload Plugin and overwrite. Check the create_lock() probe lines in the Options and Transients section.