Skip to content

Add the GitHub release updater to the bootstrap package - #27

Merged
ahegyes merged 6 commits into
trunkfrom
feat/bootstrap-github-updater
Oct 16, 2026
Merged

ahegyes merged 6 commits into
trunkfrom
feat/bootstrap-github-updater

Conversation

@ahegyes

@ahegyes ahegyes commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds register_github_updater() to the bootstrap package. A plugin whose Update URI points 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. The dws_github_updater_config_{slug} filter configures it at check time, and uninstall() deletes its cached releases on a best-effort basis.

Affected packages

  • wp-framework-bootstrap

Checklist

  • Tests added or updated (Integration: GitHubUpdaterTest, two UninstallTest rows)
  • composer quality-check passes locally (PHPCS, PHPStan and the tests), plus composer test:integration:multisite
  • Public API changes are documented in the affected package's README.md (package READMEs stay pointer-form until F14)
  • Breaking changes are explicitly justified below (none)

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
@ahegyes ahegyes changed the title Feat/bootstrap GitHub updater Add the GitHub release updater to the bootstrap package Oct 16, 2026
@ahegyes
ahegyes merged commit c383229 into trunk Oct 16, 2026
16 checks passed
@ahegyes
ahegyes deleted the feat/bootstrap-github-updater branch October 16, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant