rajandangi / wp-gh-release-updater
A self-contained, zero-configuration GitHub Release Updater Manager for WordPress plugins
Installs: 103
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 1
Open Issues: 0
pkg:composer/rajandangi/wp-gh-release-updater
Requires
- php: >=8.3
Requires (Dev)
- phpcsstandards/phpcsutils: ^1.0
- phpstan/extension-installer: ^1.3
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^9.0
- rector/rector: ^1.0
- squizlabs/php_codesniffer: ^3.7
- szepeviktor/phpstan-wordpress: ^1.3
- wp-coding-standards/wpcs: ^3.0
README
A lightweight Composer library that enables manual GitHub release updates for WordPress plugins with automatic slug detection and zero configuration.
Features
- One-Line Integration - Single class instantiation with auto-detection
- Automatic Slug Detection - Extracts slug from plugin filename
- Encrypted Token Storage - Secure token encryption using WordPress salts (AES-256-CBC)
- Data Isolation - Each plugin gets its own options, AJAX actions, and asset handles
- Namespace Scoping Ready - Works with PHP-Scoper to prevent class collisions
- Public & Private Repos - Support for both repository types
- Manual Updates Only - No automatic background checks
- Zero Dependencies - Vanilla JavaScript, no jQuery
Requirements
- WordPress 6.0+
- PHP 8.3+
- Composer 2.0+
Installation
Install via Composer in your WordPress plugin directory:
composer require rajandangi/wp-gh-release-updater
Quick Start
Integration
<?php require_once __DIR__ . '/vendor/autoload.php'; use WPGitHubReleaseUpdater\GitHubUpdaterManager; /** * Initialize GitHub Updater */ function my_plugin_github_updater() { static $updater = null; if (null === $updater) { $updater = new GitHubUpdaterManager([ 'plugin_file' => __FILE__, 'menu_title' => 'GitHub Updater', 'page_title' => 'My Plugin Updates', ]); } return $updater; } // Initialize updater my_plugin_github_updater(); // Use activation/deactivation hooks register_activation_hook(__FILE__, function() { my_plugin_github_updater()->activate(); }); register_deactivation_hook(__FILE__, function() { my_plugin_github_updater()->deactivate(); });
Configuration Options
Required Parameters:
plugin_file(string) - Path to main plugin file (__FILE__)menu_title(string) - Admin menu titlepage_title(string) - Settings page title
Optional Parameters:
menu_parent(string) - Parent menu location (default:'tools.php')capability(string) - Required capability (default:'manage_options')cli_command(string) - Base WP-CLI command path (default:<plugin-directory>)
How Auto-Detection Works
Plugin slug is extracted from filename:
my-awesome-plugin.php→my-awesome-pluginwoo-extension.php→woo-extension
This slug creates unique prefixes:
- Database:
wp_{slug}_repository_url,wp_{slug}_access_token - AJAX:
{slug}_check,{slug}_update,{slug}_test_repo - Assets:
{slug}-admin-js,{slug}-admin-css - Nonces:
{slug}_nonce
This gives each plugin its own database entries, AJAX endpoints, and script handles.
Important: Data isolation does not prevent PHP class collisions. If two plugins bundle this package at different versions without namespace scoping, only one version of each class will be loaded (whichever plugin loads first). See Avoiding Class Collisions below.
Usage
For End Users
- Go to Tools → GitHub Updater
- Enter GitHub repository (e.g.,
owner/repo) - (Optional) Add Personal Access Token for private repos
- Click Test Repository Access to verify
- Click Check for Updates to see available versions
- Click Update Now to install
WP-CLI
Each plugin instance registers a unique command path:
wp <plugin-directory> <command>
Available commands:
test-repo- same behavior as Test Repository Access in wp-admincheck-updates- checks GitHub for latest release and prints statusupdate- performs plugin update using WordPress upgrader flowdecrypt-token- decrypts and prints the currently stored access token (sensitive output)
Examples:
# Uses saved repository URL and saved token from plugin settings wp mu-plugin-updater test-repo # Check update status wp mu-plugin-updater check-updates # Perform update when available wp mu-plugin-updater update # Dry-run update flow (no upgrader execution) wp mu-plugin-updater update --dry # Decrypt and print the saved token (sensitive) wp mu-plugin-updater decrypt-token # Print only the raw decrypted token wp mu-plugin-updater decrypt-token --raw # Override repository URL for one-off testing wp mu-plugin-updater test-repo --repository-url=owner/repo # Override token for one-off testing wp mu-plugin-updater test-repo --access-token=github_pat_xxx
test-repo performs the same repository access validation as Test Repository Access in the updater settings page.
decrypt-token is intended for debugging. Treat command output as sensitive and avoid sharing it in logs.
To customize the base command and avoid naming conflicts, pass cli_command when creating the manager:
new GitHubUpdaterManager([ 'plugin_file' => __FILE__, 'menu_title' => 'GitHub Updater', 'page_title' => 'Plugin Updates', 'cli_command' => 'my-plugin-updater', ]);
For Developers
Required Plugin Headers:
<?php /** * Plugin Name: Your Plugin Name ← Required * Version: 1.0.0 ← Required * Plugin URI: https://example.com * Description: Plugin description * Author: Your Name * License: GPL v2 or later */
The updater reads Plugin Name and Version from these headers automatically. See Release Process for automating builds with GitHub Actions.
Avoiding Class Collisions with PHP-Scoper
The Problem
WordPress loads all active plugins into a single PHP process. If two plugins both require this package, PHP will only load the first version of each class it encounters (e.g., WPGitHubReleaseUpdater\GitHubUpdaterManager). The second plugin silently gets the wrong version.
This causes real issues:
- Plugin A bundles updater v1.4 (has
CLI.php,Logger.php) - Plugin B bundles updater v1.2 (missing those files)
- If Plugin B loads first, Plugin A's WP-CLI commands and logging silently break
- No error is thrown — PHP simply uses the already-loaded class
This is a fundamental WordPress limitation affecting every Composer package shared across plugins (not just this one). The built-in data isolation (unique option names, AJAX actions, etc.) does not solve this — the PHP classes themselves still collide.
The Solution: PHP-Scoper
PHP-Scoper rewrites the package's namespace at build time so each plugin gets its own copy of the classes. Plugin A uses PluginAVendor\WPGitHubReleaseUpdater\* and Plugin B uses PluginBVendor\WPGitHubReleaseUpdater\* — no collision possible.
Step 1: Install PHP-Scoper
composer require --dev humbug/php-scoper
Step 2: Create scoper.inc.php
<?php declare(strict_types=1); $finder = 'Isolated\\Symfony\\Component\\Finder\\Finder'; return [ 'prefix' => 'MyPluginVendor', // Choose a unique prefix for your plugin 'finders' => [ $finder::create() ->files() ->ignoreVCS(true) ->in(__DIR__ . '/vendor/rajandangi/wp-gh-release-updater/src'), ], // Don't prefix WordPress global functions, classes, and constants 'expose-global-constants' => true, 'expose-global-classes' => true, 'expose-global-functions' => true, // Don't prefix WordPress-bundled namespaces 'exclude-namespaces' => [ 'WP_CLI', 'WpOrg', 'PHPMailer', 'Automattic', ], ];
Step 3: Add Composer scripts
{
"autoload": {
"psr-4": {
"MyPluginVendor\\WPGitHubReleaseUpdater\\": "vendor_prefixed/rajandangi/wp-gh-release-updater/"
}
},
"scripts": {
"scope-updater": "php -d memory_limit=512M ./vendor/bin/php-scoper add-prefix --config=scoper.inc.php --output-dir=vendor_prefixed/rajandangi/wp-gh-release-updater --force",
"post-install-cmd": "@scope-updater",
"post-update-cmd": "@scope-updater"
}
}
Step 4: Update your import
// Before (unscoped — vulnerable to collisions) use WPGitHubReleaseUpdater\GitHubUpdaterManager; // After (scoped — fully isolated) use MyPluginVendor\WPGitHubReleaseUpdater\GitHubUpdaterManager;
Step 5: Run the scoper
composer run scope-updater
This generates vendor_prefixed/ with all classes rewritten under your prefix. Commit this directory to your repository so it's available in production without requiring PHP-Scoper at runtime.
Key Points
- The
expose-global-*flags prevent PHP-Scoper from prefixing WordPress core symbols (WP_Error,add_action,ABSPATH, etc.) - The
exclude-namespaceslist keeps WP-CLI and other WordPress-bundled namespaces intact - Run
composer install --no-dev --no-scriptsin CI/CD to skip scoping (sincevendor_prefixed/is already committed) - Each plugin should use a different prefix (e.g.,
AcmeVendor,MyPluginVendor)
Without PHP-Scoper
If you are certain no other active plugin on your site uses this package, you can skip scoping and use the classes directly. The data isolation (unique option names, AJAX actions, nonces) still works. Just be aware that adding another plugin with this package later will cause silent class collisions.
Security
- Token Encryption: AES-256-CBC with WordPress salts
- Strict Validation: GitHub domains only, no token leaks
- Permissions: Capability checks, nonce verification, input sanitization
Advanced Examples
Custom Menu Location
// Place under Settings menu new GitHubUpdaterManager([ 'plugin_file' => __FILE__, 'menu_title' => 'Updates', 'page_title' => 'Plugin Updates', 'menu_parent' => 'options-general.php', // Settings menu ]); // Or under Plugins menu // 'menu_parent' => 'plugins.php',
Custom Capability
// Restrict to specific capability new GitHubUpdaterManager([ 'plugin_file' => __FILE__, 'menu_title' => 'GitHub Updater', 'page_title' => 'Plugin Updates', 'capability' => 'manage_plugin_updates', // Custom capability ]);
Development
# Install dependencies composer install npm install # Run quality checks composer qa # All checks (PHPCS, PHPStan, Rector, Biome) npm run biome:check # JavaScript/CSS linting # Auto-fix issues composer fix # Fix all auto-fixable issues npm run biome:fix # Fix JS/CSS issues
Release Process
Automate your plugin's release with GitHub Actions. Push to the release branch and the workflow handles everything — version extraction, packaging, tagging, and publishing.
Setup
- Create
.github/workflows/release.ymlin your plugin repository - Update
FILE_FOR_VERSIONto your main plugin file (e.g.,my-plugin.php) - Update
SLUGto your plugin slug (e.g.,my-plugin) - Push to
releasebranch to trigger
Workflow File
.github/workflows/release.yml:
name: Build and Release Plugin on: push: branches: - release permissions: contents: write jobs: build-release: name: Build and Create Release runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up PHP uses: shivammathur/setup-php@v2 with: php-version: "8.3" tools: composer:v2 - name: Install production dependencies run: composer install --no-dev --prefer-dist --optimize-autoloader - name: Check for npm project id: check_npm run: | if [ -f "package.json" ] && ([ -f "package-lock.json" ]); then echo "has_npm=true" >> "$GITHUB_OUTPUT" else echo "has_npm=false" >> "$GITHUB_OUTPUT" fi - name: Setup Node.js if: steps.check_npm.outputs.has_npm == 'true' uses: actions/setup-node@v4 with: node-version: "lts/*" cache: "npm" - name: Install npm dependencies if: steps.check_npm.outputs.has_npm == 'true' run: npm ci - name: Build assets if: steps.check_npm.outputs.has_npm == 'true' run: npm run build - name: Determine version and plugin slug id: vars run: | # Update these variables for your plugin FILE_FOR_VERSION="my-plugin.php" # Your main plugin file SLUG="my-plugin" # Your plugin slug VERSION=$(grep -iE '^\s*\*?\s*Version:' "$FILE_FOR_VERSION" | head -n1 | sed -E 's/^.*Version:\s*//I' | tr -d '\r' | sed 's/\s*$//') if [[ -z "$VERSION" ]]; then echo "Error: Could not determine plugin version" exit 1 fi echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "slug=${SLUG}" >> "$GITHUB_OUTPUT" echo "zip=${SLUG}.zip" >> "$GITHUB_OUTPUT" - name: Create plugin package run: | mkdir -p "release-package/${{ steps.vars.outputs.slug }}" rsync -a --delete \ --exclude '.git' \ --exclude '.github' \ --exclude 'node_modules' \ --exclude 'tests' \ --exclude '.gitignore' \ --exclude 'composer.json' \ --exclude 'composer.lock' \ --exclude 'phpcs.xml' \ --exclude 'phpstan.neon' \ --exclude 'rector.php' \ --exclude 'biome.json' \ --exclude 'package.json' \ --exclude 'package-lock.json' \ --exclude 'webpack.config.js' \ --exclude 'release-package' \ ./ "release-package/${{ steps.vars.outputs.slug }}/" - name: Create ZIP file working-directory: release-package run: zip -r "${{ steps.vars.outputs.zip }}" "${{ steps.vars.outputs.slug }}" - name: Create and push tag run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git tag -a "v${{ steps.vars.outputs.version }}" -m "Release v${{ steps.vars.outputs.version }}" git push origin "v${{ steps.vars.outputs.version }}" - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: v${{ steps.vars.outputs.version }} name: v${{ steps.vars.outputs.version }} body: "Release version ${{ steps.vars.outputs.version }}" files: release-package/${{ steps.vars.outputs.zip }} generate_release_notes: true draft: false prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
How to Release
# 1. Update version in your plugin header # Version: 1.2.3 ← Update this # 2. Commit and push to release branch git add my-plugin.php git commit -m "Bump version to 1.2.3" git push origin main:release
The workflow will automatically:
- Extract the version from your plugin header
- Install production Composer dependencies
- Build frontend assets (if
package.jsonexists) - Create a clean ZIP package (excludes dev files)
- Tag the release and publish it on GitHub
Download Options
Via Composer:
composer require rajandangi/wp-gh-release-updater
Direct Download:
Grab the production-ready ZIP from the Releases page. It includes vendor/ — ready to use.
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Run QA checks (
composer qa) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
GPL v2 or later
Support
- Issues: GitHub Issues
- Changelog: See CHANGELOG.md