Contents
Supports two update sources:
- Custom server – a private
info.jsonfile you host yourself (e.g. for commercial plugins) - Public GitHub releases – reads directly from a public repo’s GitHub Releases and
readme.txt, no access token needed
How it works
class-update-checker.php only defines the Bootscore_Update_Checker class. Nothing happens until something instantiates it and registers a product. This makes the file completely safe to include anywhere, at any time – if it’s never instantiated, it’s inert.
Every consumer (theme or plugin) is expected to:
- Make sure the class is loaded (with a fallback, in case nothing else has loaded it yet)
- Instantiate the shared
$bootscore_updaterglobal, if it doesn’t already exist - Call
register_product()with its own details
All of this happens inside an add_action('init', ...) callback – not at top-level file-include time. This avoids load-order issues between the theme and plugins (WordPress plugins load before the theme’s functions.php, so relying on top-level code being “instantiated by whoever loads first” is fragile).
Because every consumer carries its own fallback, the theme does not need to explicitly load class-update-checker.phpin functions.php. It’s safe to do so for clarity/early-loading, but not required.
Registering a product
register_product() accepts:
| Key | Required | Notes |
|---|---|---|
type | yes | 'plugin' or 'theme' |
slug | yes | Unique per product. Must not be reused across products – sharing a slug means one registration silently overwrites the other. |
current_version | yes | Usually read from the plugin/theme header |
file | plugins only | plugin_basename(...) of the main plugin file |
source | yes | 'custom' or 'github' |
info_url | custom source only | Full, direct URL to info.json |
github_repo | github source only | 'owner/repo' |
name | no | Auto-generated from slug if omitted |
requires | no, github only | Min WP version override. custom source always reads this from info.json. |
tested | no, github only | “Tested up to” override. Falls back to the repo’s readme.txt, then the site’s own WP version. |
requires_php | no, github only | Min PHP version override |
Case 1 – Plugin, private server (info.json)
Used for commercial plugins hosted on your own server.
inc/updater-config.php (required by the plugin’s main file):
<?php
defined('ABSPATH') || exit;
global $bootscore_updater;
// Ensure the class is loaded
if (!class_exists('Bootscore_Update_Checker')) {
$theme_path = get_template_directory() . '/inc/updater/class-update-checker.php';
if (file_exists($theme_path)) {
require_once $theme_path;
}
}
// Initialize if class exists
if (!isset($bootscore_updater) && class_exists('Bootscore_Update_Checker')) {
$bootscore_updater = new Bootscore_Update_Checker(12 * HOUR_IN_SECONDS);
}
// Only proceed if updater is ready
if (!isset($bootscore_updater) || !class_exists('Bootscore_Update_Checker')) {
return; // Silent fail if updater not available
}
add_action('init', function() {
global $bootscore_updater;
if (!class_exists('Bootscore_Update_Checker') || !$bootscore_updater) {
return;
}
$plugin_file = dirname(__DIR__) . '/main.php';
if (!file_exists($plugin_file)) {
return;
}
$plugin_data = get_file_data(
$plugin_file,
array('Version' => 'Version')
);
$bootscore_updater->register_product(array(
'type' => 'plugin',
'slug' => 'bs-example-plugin',
'current_version' => $plugin_data['Version'] ?? '1.0',
'file' => plugin_basename($plugin_file),
'source' => 'custom',
'info_url' => 'https://files.bootscore.me/plugins/bs-example-plugin-XXXX-XXXX/info.json',
'name' => 'bs Example Plugin',
));
});
info.json (hosted at the info_url above):
{
"name" : "bs Example Plugin",
"slug" : "bs-example-plugin",
"author" : "<a href='https://v7-dev.bootscore.me/'>Bootscore</a>",
"author_profile" : "https://v7-dev.bootscore.me/",
"version" : "1.3.0",
"download_url" : "https://files.bootscore.me/plugins/bs-example-plugin-XXXX-XXXX/bs-example-plugin.zip",
"requires" : "5.0",
"tested" : "7.0",
"requires_php" : "7.4",
"last_updated" : "2026-06-19",
"sections" : {
"description" : "<h4>bs Example Plugin</h4><p>Plugin description here.</p>",
"installation" : "<h4>Installation</h4><ol><li>In your admin panel, go to Plugins > and click the Add New button.</li><li>Click Upload Plugin and Choose File, then select the Plugin's .zip file. Click Install Now.</li><li>Click Activate to use your new Plugin right away.</li></ol>",
"changelog" : "<h4>Changelog</h4><p>You can find a detailed changelog in the readme.txt file in the plugin's root directory.</p>"
},
"banners" : {
"low" : "https://files.bootscore.me/plugins/banner_sm.png",
"high" : "https://files.bootscore.me/plugins/banner_lg.png"
}
}
Case 2 – Plugin, public GitHub release
Used for free plugins with public repos. No token required.
inc/updater-config.php:
<?php
defined('ABSPATH') || exit;
add_action('init', function() {
global $bootscore_updater;
// Ensure the class is loaded
if (!class_exists('Bootscore_Update_Checker')) {
$theme_path = get_template_directory() . '/inc/updater/class-update-checker.php';
if (file_exists($theme_path)) {
require_once $theme_path;
}
}
// Initialize if needed
if (!isset($bootscore_updater) && class_exists('Bootscore_Update_Checker')) {
$bootscore_updater = new Bootscore_Update_Checker(12 * HOUR_IN_SECONDS);
}
if (!isset($bootscore_updater) || !class_exists('Bootscore_Update_Checker')) {
return;
}
$plugin_file = dirname(__DIR__) . '/main.php';
if (!file_exists($plugin_file)) {
return;
}
$plugin_data = get_file_data(
$plugin_file,
array('Version' => 'Version')
);
$bootscore_updater->register_product(array(
'type' => 'plugin',
'slug' => 'bs-example-free-plugin',
'current_version' => $plugin_data['Version'] ?? '1.0',
'file' => plugin_basename($plugin_file),
'source' => 'github',
'github_repo' => 'bootscore/bs-example-free-plugin',
'name' => 'bs Example Free Plugin',
));
});
No info.json needed. Instead, the plugin’s repo should contain a standard WordPress-format readme.txt at its root:
=== bs Example Free Plugin ===
Stable tag: 1.3.0
Tested up to: 7.0
Requires at least: 5.0
Requires PHP: 7.4
License: MIT License
License URI: license-url
== Description ==
This is the long description. Supports basic Markdown-lite: paragraphs and `- ` bullet lists.
== Installation ==
1. In your admin panel, go to Plugins > and click the Add New button.
2. Click Upload Plugin and Choose File, then select the Plugin's .zip file. Click Install Now.
3. Click Activate to use your new Plugin right away.
== Changelog ==
= 1.3.0 - June 19 2026 =
#### Bugfix
- Fixed a bug
#### Update
- Added icon.svg
= 1.2.0 - May 21 2026 =
#### Updates
- Some update
How it’s resolved, in order:
- The GitHub Release’s zip asset — an asset named exactly
{slug}.zipis preferred; falls back to the first.zipasset found, then to the standardhttps://github.com/{repo}/releases/download/{tag}/{slug}.zippattern. readme.txtfetched at the exact release tag (notmain) provides the Description / Installation / Changelog tabs and theTested up to/Requires at least/Requires PHPvalues.- If no
readme.txtis found, falls back to parsing the GitHub Release’s own description box, if it contains## Description/## Changelogheadings. - If neither is available: sensible defaults (
5.0/7.4/ site’s own WP version).
Version tags: a leading v/V is stripped automatically (v1.3.0 → 1.3.0).
Icons: same as Case 1 — assets/icon.svg bundled in the plugin is used automatically.
Case 3 – Theme, private server (info.json)
Same pattern as Case 1, just 'type' => 'theme' and no file key (unused for themes).
inc/updater/updater-config.php:
<?php
defined('ABSPATH') || exit;
global $bootscore_updater;
if (!class_exists('Bootscore_Update_Checker')) {
$class_path = get_template_directory() . '/inc/updater/class-update-checker.php';
if (file_exists($class_path)) {
require_once $class_path;
}
}
if (!isset($bootscore_updater) && class_exists('Bootscore_Update_Checker')) {
$bootscore_updater = new Bootscore_Update_Checker(12 * HOUR_IN_SECONDS);
}
if (!isset($bootscore_updater) || !class_exists('Bootscore_Update_Checker')) {
return;
}
add_action('init', function() {
global $bootscore_updater;
if (!class_exists('Bootscore_Update_Checker') || !$bootscore_updater) {
return;
}
$theme = wp_get_theme('bootscore');
$bootscore_updater->register_product(array(
'type' => 'theme',
'slug' => 'bootscore',
'current_version' => $theme->get('Version') ?: '1.0.0',
'source' => 'custom',
'info_url' => 'https://files.bootscore.me/themes/bootscore/info.json',
'name' => 'Bootscore',
));
});
inc/theme-setup.php / functions.php:
// Load theme's own update configuration
require_once get_template_directory() . '/inc/updater/updater-config.php';
This one line is the only thing that needs to run unconditionally — everything else (class loading, instantiation) is self-contained inside updater-config.php.
info.json: same structure as Case 1, adjusted for the theme (theme screenshots can be used as banners if desired).
Case 4 – Theme, public GitHub release
inc/updater/updater-config.php:
<?php
defined('ABSPATH') || exit;
add_action('init', function() {
global $bootscore_updater;
if (!class_exists('Bootscore_Update_Checker')) {
$class_path = get_template_directory() . '/inc/updater/class-update-checker.php';
if (file_exists($class_path)) {
require_once $class_path;
}
}
if (!isset($bootscore_updater) && class_exists('Bootscore_Update_Checker')) {
$bootscore_updater = new Bootscore_Update_Checker(12 * HOUR_IN_SECONDS);
}
if (!isset($bootscore_updater) || !class_exists('Bootscore_Update_Checker')) {
return;
}
$theme = wp_get_theme('bootscore');
$bootscore_updater->register_product(array(
'type' => 'theme',
'slug' => 'bootscore',
'current_version' => $theme->get('Version') ?: '1.0.0',
'source' => 'github',
'github_repo' => 'bootscore/bootscore',
'name' => 'Bootscore',
));
});
// functions.php
require_once get_template_directory() . '/inc/updater/updater-config.php';
No info.json needed – same readme.txt-first resolution as Case 2 applies. The theme’s own screenshot.png (standard WordPress theme convention) is used automatically for the theme details screen; no separate icon/banner config needed.
Cache & clearing
Remote data is cached in WordPress transients for the duration passed to the constructor (12 * HOUR_IN_SECONDS in all examples above):
- Custom server:
bootscore_custom_{slug} - GitHub:
bootscore_github_{slug}
Cache is cleared automatically after any update is installed (upgrader_process_complete). To force a fresh check manually during development/testing:
wp transient delete bootscore_custom_bs-example-plugin
wp transient delete bootscore_github_bs-example-free-plugin
Migration note (PUC → this updater)
This library and the PluginUpdateChecker (PUC) library can coexist safely during migration, since:
class-update-checker.phponly defines a class; it does nothing until instantiated- Each system only writes to the transient/
plugins_apiresponse entries for the products it has registered — there’s no shared state for products registered with only one system