Repository navigation
feat: update-core applies the release WordPress offers (#351) - #357
Merged
Merged
Conversation
Rewrite the hard line before any code: Saddle installs a plugin or theme only from the WordPress.org directory, by slug, through WordPress's own installer, and the new install arrives inactive. Core updates are minor and security releases, plus a major one only behind WordPress's own switch for all new versions. Translations go through core's language pack upgrader. Nothing deletes a plugin or theme. Adds the two "What NOT to do" bullets and the ROADMAP entry. Refs #347 Co-Authored-By: Claude Opus 5.5 <[email protected]>
New admin-tier, read-only tool. It asks the WordPress.org directory through core's own plugins_api(), the same request Plugins > Add New makes, by search words or one exact slug, at most 10 results. Each row has the slug, name, author, version, active installs, rating, last update, tested up to, the WordPress and PHP it needs, and whether it is installed and active here, by core's install_plugin_install_status(). The directory answer is cached for an hour; the installed state never is. Tests answer the directory from responses recorded from the live API, which pins the field names. Refs #348 Co-Authored-By: Claude Opus 5.5 <[email protected]>
New admin-tier, gated tool. One slug per call. The preview asks the directory fresh and shows the version, installs, rating, last update, tested-up-to against this WordPress and the PHP it needs, with warnings for untested, two years without an update, or plugins it needs. The token binds the version. Refused before any token: a site that forbids file changes, an account without install_plugins, an unknown or closed slug, an installed plugin or a leftover folder, PHP or WordPress too old, and a package outside downloads.wordpress.org. A confirm queues the install on the existing runner; the request never calls the installer (pinned, and the update-plugin test now counts upgrader calls too). Saddle_Install_Runner asks the directory again, installs only the confirmed version through Plugin_Upgrader::install(), and the plugin arrives inactive. Saddle_Directory holds the lookups and refusals both sides need. The installed check reads core's rule without install_plugin_install_status(), which forces an update check. Refs #349 Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ated Same shape as install-plugin: one slug, a fresh directory answer through themes_api(), a preview with the version, installs, rating, last update and the WordPress and PHP it needs, a token bound to the version, a background run through Theme_Upgrader::install(). The theme is never activated. The permission capability is switch_themes and the handler checks install_themes after naming any blocked_by() reason. Core's theme installer fetches a missing parent theme on its own. A child theme whose parent is not installed is refused before a token, the run checks again (the folder too, since WP_Theme can answer from the object cache after a delete), and during the run the directory answers only the confirmed slug. Saddle_Directory now only talks to the API. The installed check and the refusals moved to Saddle_Install_Runner, where the run and the abilities both reach them, as blocked_by() sits on the update runner. Refs #350 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Found end to end on Playground: install-theme over MCP failed with "Call to undefined function themes_api()". The update runner's loader keys the admin theme.php on wp_get_themes(), which wp-includes always defines, so a REST request never loaded it. The test suite loads the admin files up front, which hid it. Refs #350 Co-Authored-By: Claude Opus 5.5 <[email protected]>
New admin-tier, gated tool. It reads the "autoupdate" entries of the update_core transient, the releases WordPress would apply in the background (get_core_updates() leaves them out), and picks the newest minor release, else a major one only when Core_Upgrader says WordPress would apply it itself. The preview shows the from and to version, minor or major, the PHP and MySQL needed, and for a major the active plugins not tested with it and what cannot be undone. The token binds the release. Refused before a token: blocked_by() reasons, no update_core, a development version, a recorded critical failure, PHP or MySQL too old, a major WordPress would not apply, nothing offered. Found while testing: since WordPress 5.6 a new install defaults auto_update_core_major to "enabled", so on most sites WordPress's own switch already allows majors. The copy now says "when WordPress is set to update itself to all new versions" instead of "when the owner turned it on", and a test pins core's default. The runner was at 461 lines, so the offer path (offers, the one-shot auto_update filter, the apply loop, result recording) moved to Saddle_Offer_Runner beside Saddle_Install_Runner; Saddle_Update_Runner keeps scheduling, guards, the lock and the run store (304 lines). The offer path also closes the output buffer core's skin opens on failure. Refs #351 Co-Authored-By: Claude Opus 5.5 <[email protected]>
3 of 4 tasks
This was referenced Oct 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #351
Stacked on #356 (base
feat/350-install-theme). Merge #353 → #354 → #355 → #356 → this, with merge commits.What
A new tool,
saddle/update-core. It updates WordPress itself to a release WordPress already offers for background updates, through core's own automatic updater in the background runner, after a confirmed preview.Why
#347: core updates join plugin and theme updates. Fahim's decision (2026-10-12): minor and security releases always. A major only behind WordPress's own "automatic updates for all new versions" switch, which no tool can flip.
Since WordPress 5.6, a new install defaults that switch to on (
auto_update_core_major=enabledinschema.php). Sites upgraded from before 5.6 haveunset, and an owner who picked "maintenance and security releases only" hasdisabled; both mean minor only.So on most modern sites,
update-corewill apply a major release when asked. This is exactly what WordPress would do by itself within about 12 hours. Saddle just does it now, after a confirmed preview. It is refused only where the owner (or an old install) has majors off.That matches the rule you chose. Your expectation may have been that majors are usually refused, so I changed the copy to say "when WordPress is set to update itself to all new versions" rather than "when the owner turned it on", and a test pins core's default. If you want majors to need a separate Saddle opt-in instead, that's a small change. Say so.
How
autoupdateentries of theupdate_coretransient: the releases WordPress would apply in the background.get_core_updates()leaves those out;find_core_auto_update()reads the same. It picks the newest minor release, else a major only ifCore_Upgrader::should_update_to_version()says WordPress would apply it itself (the switch,WP_AUTO_UPDATE_CORE,allow_major_auto_core_updates). When both are offered, the minor comes first.blocked_by('core')(contextABSPATH)update_coreauto_core_update_failedauto_update_corefilter says yes for exactly the confirmed version, andWP_Automatic_Updater::update( 'core', … )applies it.Saddle_Update_Runnerhad reached 461 lines. The offer path (offers, the one-shot filter, the apply loop, result recording) moved toSaddle_Offer_Runner(180 lines), besideSaddle_Install_Runner.Saddle_Update_Runnerkeeps scheduling, guards, the lock and the run store (304 lines), and its public API is unchanged. The offer path also closes the output buffer core's skin opens on a failure (WP_Upgrader_Skin::error()callsheader()).Testing
phpuniton PHP 8.2: 1503 tests, 8570 assertions, 1 skipped (the existing 11-plugin test).phpcson every changed PHP file: clean.tests/core-updates-test.php(13 tests). Offers are built on a version-check answer recorded live, which pins the field names. Covered:up_to_datelist-updatesoffered 7.1.3.update-corepreview said minor, 7.1.2 to 7.1.3.applied, andget-site-inforeports 7.1.3.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.