Skip to content

No supported way for WP-CLI / headless update managers to receive Freemius updates into the update_plugins transient #872

Description

@TheDarkMist

Summary

There is no supported way to make a Freemius-powered premium update appear in WordPress core's update_plugins transient from a non-admin, non-cron context (WP-CLI, server-side update managers such as Plesk/cPanel WP Toolkit, and intermittently via remote management dashboards). As a result wp plugin update <slug> reports no update and installs nothing, and bulk update tooling shows stale or missing updates. For security releases this means sites silently stay unpatched.

Context

We are a WordPress agency managing many client sites and patching them centrally (WP-CLI and a remote management dashboard, on cPanel/CloudLinux). A recent security release of a Freemius-powered premium plugin (a fix for an RCE in older versions) had to be rolled out fleet-wide and fast. The standard tooling could not be trusted to apply it.

Observed behavior

  • wp plugin update <slug> reports the plugin is already up to date, or finds no update, even when a newer premium version exists for the license.
  • Remote/bulk update tools intermittently fail to detect or apply the update.
  • Server-side update managers (we have also seen this with Plesk/cPanel WP Toolkit) appear to be affected by the same root cause, since they run outside an admin session.

Why (from reading the SDK, current master)

  • The updater registers pre_set_site_transient_update_plugins_filter in includes/class-fs-plugin-updater.php, which calls $this->_fs->get_update( false, fs_request_get_bool( 'force-check' ) ). Outside an admin request, force-check is false, so get_update() is served from the local cache (UPDATES_CHECK_CACHE_EXPIRATION).
  • The data sync that refreshes that cache is gated to an admin/non-cron context. In includes/class-freemius.php: if ( $this->has_api_connectivity() && ! self::is_cron() && $this->is_user_in_admin() ) { ... }. Under WP-CLI, is_user_in_admin() is false and DOING_CRON is not defined, so the cache is never refreshed headlessly.
  • Your documentation states the transient is populated "after visiting the Account page", and that updates "can only work when the product is actually active". There is no documented WP-CLI / headless path.

Workaround we had to use (illustrates the gap)

To get a fresh update into the transient under WP-CLI we had to, per site: load the upgrader includes, call a private method via reflection (get_update with the flush flag set to true) to force a fresh check, re-run pre_set_site_transient_update_plugins_filter manually and write the result back with set_site_transient('update_plugins', ...), then run the core Plugin_Upgrader. Because updates are served step-by-step, far-behind installs needed several passes. This is brittle and relies on private API.

Impact

Agencies that patch many sites centrally cannot rely on WP-CLI, server-side update managers (e.g. WP Toolkit), or remote management tooling to apply Freemius-powered updates. This is most serious for security releases: the mass-update tooling returns false or no-op results and sites stay vulnerable.

Request

A supported, public way to refresh and inject Freemius updates in headless contexts, for example any of:

  1. A first-class WP-CLI command (or documented recipe) to force a fresh update check and populate the update_plugins transient.
  2. A public action/filter or method to force a fresh update check outside admin request params (so wp plugin update works without reflection on private methods).
  3. Documentation of the supported approach for WP-CLI / remote management at scale.

Separately, the step-by-step delivery multiplies the number of passes needed for far-behind installs during a mass update. An option to jump straight to the latest release, at least for security releases, would materially help agencies patch quickly.

Environment

WP-CLI 2.x, WordPress 6.9, PHP 8.x, cPanel/CloudLinux, plugin bundling a current Freemius SDK 2.x. Happy to share detailed logs privately.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions