Repository navigation
Add the GitHub release updater to the bootstrap package - #27
Merged
Merged
Conversation
The bootstrap package has no updater yet: a plugin whose Update URI
points at github.com gets no update offers, and the uninstall engine
does not delete the release caches an updater keeps.
GitHubUpdaterTest covers the updater through WordPress's own plugin
update check and plugins_api(), with every request answered through
pre_http_request: the stable and prerelease channels, unusable releases
and their five-minute retry, the requirements an offer carries and the
one that withholds it, other plugins' checks and details requests, and
the dws_github_updater_config_{slug} filter. UninstallTest gains a row
that deletes the updater's caches whatever the consent, and its fixtures
now write the plugin's entry file, whose Text Domain names those caches.
The updater tests fail with "Call to undefined function
register_github_updater()", and the uninstall row fails on both data
sets.
Assisted-by: Claude Code:claude-opus-5-5
The updater tests left six behaviors unpinned: a configuration filter that returns only the keys it changes, a repository with a trailing newline, a plugin without a Text Domain, a cached release record without a package, a requirement header followed by the comment's closing */ on the same line, and an uninstall whose delete of a cached release fails. The configuration row now returns only the repository, asset and channel; the unusable configurations gain the trailing newline; the no-updates rows gain a plugin without a Text Domain; a new row seeds a cached record without a package and expects a fresh fetch; and every staged entry file closes its comment on the last header's line. UninstallTest gains a row whose cache delete fails: the run throws, naming the transient, and the retry deletes it. The updater tests still fail with "Call to undefined function register_github_updater()". Assisted-by: Claude Code:claude-opus-5-5
A plugin installed from a GitHub release was never offered a newer one:
WordPress asks wordpress.org, plus the filter of the plugin's Update URI
host, and the bootstrap package answered none.
register_github_updater() answers update_plugins_github.com for the
plugin's own basename when its Update URI points at github.com, and only
when no earlier filter answered. A version without a hyphen follows
releases/latest; a version with one takes the highest of the ten
releases GitHub lists first, drafts skipped and stable releases
included. The package is the release asset named after the Text Domain,
and the requirements come from the entry file at the release's tag. The
offer carries both requirements, and a release that needs a newer
WordPress is not offered, because WordPress's automatic updates check
only the PHP requirement. Each channel's release is cached in a
transient named after the Text Domain, for an hour, or for five minutes
after a failed fetch or an unusable release (no asset of that name, or
an unreadable entry file). plugins_api answers "View details" for the
plugin's own slug from the same release, and without one answers
locally with no sections, so the package ships no string.
The dws_github_updater_config_{slug} filter runs at check time with the
defaults (enabled, repository, asset, channel) and the plugin's
basename; a key it leaves out keeps its default, and an unusable
configuration means no request, no offer and no details answer.
uninstall() deletes both channels' cached releases on every site pass,
whatever the consent, and throws a failed delete like any other.
load.php loads updater.php, and AGENTS.md gains L11: offers come only
from WordPress's own update check, so a failing wordpress.org request
delays them, and a plugin active only on non-main sites never gets one.
Assisted-by: Claude Code:claude-opus-5-5
Review found five faults in the updater. WordPress prints a release's PHP requirement unescaped on its details screen, so a tagged entry file could inject markup there. PHP's version_compare() ranks words such as 'preview' above every number, so a non-version tag among the releases would be offered to every site. Without a release, the details answer had no sections, which WordPress's details screen reads without a guard (four PHP warnings per view). WordPress fires update_plugins_github.com for an Update URI without a scheme, because it reads the host after sanitizing the URI, but the updater read the raw value and offered nothing. And the registration docblock called the updater inert below WordPress 5.8, while its details answer works there. A requirement that is not a version now makes the release unusable, and a tag that is not a version is skipped, so the channel caches an empty result for five minutes when no other release qualifies. The local details answer carries empty sections. The host and the default repository are read from esc_url_raw() of the Update URI, as WordPress 5.8 and later read it. The docblocks say what holds: below 5.8 no update is offered, uninstall's default arguments keep the footprint, and a key the filter leaves out keeps its default; the entry-file comment reads as a sentence. AGENTS.md convention 8 names the updater's caches as the exception to the prefix rule. New test rows cover a scheme-less Update URI, a word tag on each channel, a requirement carrying markup and the empty sections. Assisted-by: Claude Code:claude-opus-5-5
A failed delete of a cached release was thrown like a failed delete of the footprint, so a database fault on a row that expires within an hour made even a retaining uninstall fail, and WordPress kept the plugin's files. The retry could not finish either: once the value row is gone, delete_transient() never deletes the timeout row. The engine now calls delete_transient() for each channel outside the failure tracking, and a cached release it cannot delete is left to expire, as the uninstall() docblock now says. The failing-delete row asserts that the run throws nothing and still deletes the footprint and the consent row. Assisted-by: Claude Code:claude-opus-5-5
read_release()'s docblock promised null for any field that is not a string, but the record reads a null field as empty, as it reads a missing one. The summary now names missing and null fields together and reserves null for a field of another type. Assisted-by: Claude Code:claude-opus-5-5
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.
Summary
Adds
register_github_updater()to the bootstrap package. A plugin whoseUpdate URIpoints at github.com is offered its GitHub releases through WordPress's own update screens: a stable version follows the latest release, and a pre-release version the highest of the ten newest. Each release is packaged as the asset named after the plugin's Text Domain and carries the tagged entry file's requirements, and a release that needs a newer WordPress is not offered. "View details" answers from the same cached release. Thedws_github_updater_config_{slug}filter configures it at check time, anduninstall()deletes its cached releases on a best-effort basis.Affected packages
wp-framework-bootstrapChecklist
GitHubUpdaterTest, twoUninstallTestrows)composer quality-checkpasses locally (PHPCS, PHPStan and the tests), pluscomposer test:integration:multisiteREADME.md(package READMEs stay pointer-form until F14)