Packaging and Releases
This page covers how to publish a GitHub release that the updater will recognize, what the zip must contain, how the release notes are shown, and what to do when more than one plugin or theme on a site bundles the library.
Release Rules
The updater reads the published releases of the repository and their assets. GitHub generates a source code archive for every release, but the updater doesn't use it. The updater installs only the zip archive that you upload to the release as an asset, described under Release Asset. For more about releases and tags, see the GitHub documentation on releases and tags.
The updater can select a release only when all of the following are true.
- The release isn't a draft.
- The release isn't marked as a pre-release on GitHub.
- The tag of the release is a lowercase
vfollowed by three numbers separated by dots, such asv1.2.3, with no leading zeros in a number.
The updater selects only stable releases. Under Semantic Versioning, a version with a suffix after the three numbers is a pre-release. A release with a tag such as v1.2.3-beta.1 therefore won't be selected, even when the release isn't a draft and isn't marked as a pre-release on GitHub.
The updater retrieves the 30 most recent releases for the repository from GitHub. Of those, it selects the release with the highest version that meets all of these conditions, compared as version numbers, so v1.10.0 is higher than v1.9.0. The date of a release and its position in the list don't matter.
Release Asset
The release must contain exactly one asset that is a zip archive with a name that starts with the slug and a hyphen, and ends in .zip. The slug is the name of the plugin or theme folder, so a plugin in the example-plugin folder uses an asset such as example-plugin-1.2.3.zip. The name is case sensitive, and the updater doesn't read the version from it.
Other assets, such as a checksum file, are ignored. A second zip archive whose name also starts with the slug and a hyphen, such as example-plugin-docs.zip, makes the release ambiguous. The updater doesn't know how to choose between the two, so the release can't be installed.
When the release with the highest version has no asset with a matching name, or has more than one, the updater doesn't fall back to an older release, so WordPress shows no update. The updater-provided settings section, which shows the status of the last check, will display the message "The newest release is not set up correctly, so it cannot be installed." Add the missing asset or remove the extra one, then click Check for updates now, or wait an hour for the next check. See Troubleshooting for the meaning of other messages.
Custom Asset Names
If the assets of your releases are named another way, tell the updater which name to look for with assetPattern(). The default is <slug>-*.zip. In the value, * stands for any run of characters, ? stands for one character, and a set of characters in square brackets, such as [0-9], stands for one character from the set. A name that contains a square bracket can't be matched literally, so use ? in its place. For example, a plugin in the example-plugin folder that publishes exampleplugin-1.2.3.zip uses this setup.
Updater::plugin(__FILE__)
->fromGitHub('owner/repository')
->prefix('exampleplugin')
->assetPattern('exampleplugin-*.zip')
->register();
The value is a file name pattern, so it can't be empty or contain a slash. See the Reference for the full signature.
Zip Contents
The zip contains a single top-level folder, and the folder is named after the slug. WordPress installs an update into the folder named inside the zip, and the updater rejects an unpacked update whose folder has any other name. Because the plugin or theme loads the Composer autoloader, the folder must contain the files of the plugin or theme and the vendor folder with the runtime dependencies.
example-plugin-1.2.3.zip
example-plugin/
example-plugin.php
vendor/
The Version header (either in the main file of the plugin or in the style.css file of the theme) must match the version in the tag. The updater doesn't compare them, but WordPress compares the Version header of the installed plugin or theme with the version of the tag, so a zip whose Version header is 1.2.2 for the v1.2.3 tag installs, causing WordPress to then show the same update again.
Building the Zip
Using the accompanying packaging script and reusable GitHub Actions workflow from the scottoffen/workflows repository is recommended but not required. The README of that repository describes how they work and how to set them up. Any other process that produces the zip described above works too.
Release Notes
The notes on the GitHub release are written in Markdown and converted to HTML when they are shown. A plugin shows them in the details popup, and a theme shows them on a separate page. A release with no notes shows a short message saying that none were published. Release notes appear in the update details, so review them before you publish a release.
The popup of a plugin is built from the Plugin Name, Author, and Plugin URI headers in the main file, the date that the release was published, and the notes in a Changelog section. It always describes the latest available release, regardless of the text on the link used to open it.
Images in the notes load only from github.com and githubusercontent.com hosts. Any other image becomes a link.
WordPress allows only a narrow set of HTML in the details popup of a plugin. Tables, <details> blocks, strikethrough, and horizontal rules show as plain text there. WordPress also removes the colors of GitHub's alert boxes (NOTE, TIP, IMPORTANT, WARNING, and CAUTION), so the updater adds a small stylesheet that restores them. It prints the stylesheet only on the popup of that plugin. The details link of a theme update opens a separate page that isn't limited to that set, so tables, strikethrough, horizontal rules, and the colors of alert boxes appear there.
The popup and the theme page show the notes stored by the last check and never send a request to GitHub. A change to the notes on GitHub won't appear until after the next check. That is when the stored result is more than 12 hours old and WordPress checks for updates, or when someone clicks Check for updates now in the updater-provided settings section.
Bundling in Multiple Plugins and Themes
Each plugin or theme that uses the updater includes its own copy in its vendor folder, and every copy uses the same class names. PHP declares a class once per request, so one copy of each class serves every plugin and theme on the site, and that copy is determined by the order in which they load. Unless you rewrite the namespace of the library in your own build step, keep every copy on a site at the same version.
A build step that rewrites the namespace gives a package its own copy of the library. That package can then use any version, independent of the others.
The following requirements apply to any tool that rewrites the namespace.
- Rewrite the namespaces of the library and of
scottoffen/markdown-converterin the same build step. The library imports the namespace of the converter. - Leave the WordPress functions and classes unrenamed. The library calls functions such as
wp_safe_remote_get()and uses classes such asWP_Error, and WordPress declares them without a prefix. - Use the rewritten name in your own code. That includes the call to the
uninstall()method in theuninstall.phpfile of a plugin.
Strauss
The following composer.json setting configures Strauss to copy the library and the Markdown converter that it requires into the vendor-prefixed folder and to prefix the namespace of each. Replace ExamplePlugin with a name that is unique to your plugin or theme.
{
"extra": {
"strauss": {
"target_directory": "vendor-prefixed",
"namespace_prefix": "ExamplePlugin\\Vendor\\",
"packages": [
"scottoffen/wp-github-updater",
"scottoffen/markdown-converter"
],
"update_call_sites": true
}
}
}
Strauss adds the namespace_prefix value to the front of each namespace, so the Updater class becomes ExamplePlugin\Vendor\ScottOffen\WpGithubUpdater\Updater. When update_call_sites is true, Strauss also changes the references in the files and folders of the autoload key in your composer.json.
The uninstall.php file of a plugin isn't in the autoload key, so Strauss doesn't change the uninstall() call in it when update_call_sites is true. To change that call, set update_call_sites to an array of paths that includes the uninstall.php file and the paths in the autoload key.
PHP-Scoper
PHP-Scoper also rewrites namespaces. Its documentation has a section on WordPress support that explains how to exclude the symbols that WordPress declares.