Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
name: CI

# Runs on every pull request and every push to main, so a change is checked
# before a tag is pushed. create-release.yml keeps its own test run as a last
# gate for the tag.

permissions:
contents: read

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
quality:
name: Code standards and static analysis
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup PHP
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
with:
php-version: "8.2"
extensions: mbstring, xml, ctype, json, tokenizer
coverage: none
tools: composer:v2

# The SilverAssist development dependencies come from GitHub vcs
# repositories, so composer needs a token (see README, "Installing via
# Composer").
- name: Install Composer dependencies
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
run: composer install --no-interaction --no-progress --optimize-autoloader

- name: Validate composer.json
run: composer validate --strict

- name: PHP CodeSniffer
run: composer phpcs

- name: PHPStan
run: composer phpstan

tests-wordpress:
name: Tests on the WordPress test suite (PHP ${{ matrix.php-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-version: ["8.2", "8.3", "8.4"]

steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup PHP
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, xml, ctype, json, tokenizer, mysqli
coverage: none
tools: composer:v2

- name: Setup MySQL
run: |
sudo systemctl start mysql.service
mysql -e "CREATE DATABASE IF NOT EXISTS wordpress_test;" -uroot -proot
mysql -e "CREATE USER IF NOT EXISTS 'wp_test'@'localhost' IDENTIFIED BY 'wp_test';" -uroot -proot
mysql -e "GRANT ALL PRIVILEGES ON wordpress_test.* TO 'wp_test'@'localhost';" -uroot -proot
mysql -e "FLUSH PRIVILEGES;" -uroot -proot

- name: Install Subversion (required by the WordPress test suite)
run: |
sudo apt-get update
sudo apt-get install -y subversion

- name: Install Composer dependencies
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
run: composer install --no-interaction --no-progress --optimize-autoloader

- name: Install WordPress test suite
run: bash scripts/install-wp-tests.sh wordpress_test wp_test wp_test localhost latest

# WP_TESTS_DIR switches tests/bootstrap.php to the real WordPress test
# suite, so nothing from WordPress core is mocked and every suite
# (unit, integration, wordpress) runs against real core.
- name: PHPUnit (real WordPress)
env:
WP_TESTS_DIR: /tmp/wordpress-tests-lib
run: ./vendor/bin/phpunit --configuration=phpunit.xml

tests-standalone:
name: Unit tests without WordPress
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup PHP
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
with:
php-version: "8.2"
extensions: mbstring, xml, ctype, json, tokenizer
coverage: none
tools: composer:v2

- name: Install Composer dependencies
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
run: composer install --no-interaction --no-progress --optimize-autoloader

# Without WP_TESTS_DIR the bootstrap loads tests/wordpress-mocks.php,
# which is how a contributor without a database runs the suite.
- name: PHPUnit (standalone)
run: ./vendor/bin/phpunit --configuration=phpunit.xml --testsuite unit,integration
2 changes: 2 additions & 0 deletions .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ jobs:
echo "✅ Subversion installed"

- name: Install Composer dependencies
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
run: |
composer install --optimize-autoloader --no-interaction
echo "✅ Composer dependencies installed successfully (including dev dependencies for testing)"
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Changelog

## [1.4.0] - 2026-09-25

### Added

- **Private repositories.** The updater reads the releases of a private GitHub repository with a token. The `token_constant` option (default `SILVER_GITHUB_TOKEN`) names a PHP constant or environment variable, and `UpdaterConfig::getGithubToken()` returns it, trimmed, or `null`. It is never read from the database.
- The token is sent as `Authorization: Bearer` to `api.github.com` only. A private asset is downloaded through its API URL in two steps: the first request carries the token and stops at GitHub's redirect, the second goes to the signed storage URL without the token. A redirect that is not https is refused.
- Failed requests log a distinct message for 401, 403 and 404, with and without a token, naming the constant to check. The token is never logged, and a failed version lookup is not cached.
- Tests on the real WordPress Test Suite that intercept requests with the `pre_http_request` filter, and an opt-in live test against a private repository (`WPGU_LIVE_REPO`, `WPGU_LIVE_VERSION`, `SILVER_GITHUB_TOKEN`).
- `ci.yml` runs on every pull request and push to `main`: PHPCS, PHPStan and `composer validate --strict`, the full PHPUnit suite on the real WordPress Test Suite (PHP 8.2, 8.3 and 8.4), and the unit and integration suites without WordPress. Until now the tests only ran when a release tag was pushed.

### Fixed

- `composer phpstan` crashed on the default 128 MB PHP memory limit. It now runs with `--memory-limit=512M`.
- 29 PHPStan level 8 errors, and an `ignoreErrors` pattern that no longer matched, so `composer check` passes again.
- `download_link` in the plugin information is empty, instead of a broken URL, when the version lookup fails.
- `pluginInfo()` and `checkForUpdate()` no longer raise a warning on a missing `slug` or a transient that is not an object.
- The Markdown to HTML conversion keeps the original text when a regular expression fails, instead of dropping it.

### Changed

- Documentation, class descriptions and the package description no longer say "public GitHub releases".
- `ci.yml` and `create-release.yml` pass the `COMPOSER_AUTH` secret to `composer install`, because the SilverAssist development dependencies are resolved from GitHub.

### Documentation

- The README listed `8.3` as the default of `requires_php`, the code defaults to `8.2`.

## [1.3.2] - 2026-09-25

### Changed
Expand Down
70 changes: 67 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Software License](https://img.shields.io/badge/license-PolyForm--Noncommercial--1.0.0-blue.svg?style=flat-square)](LICENSE.md)
[![Total Downloads](https://img.shields.io/packagist/dt/silverassist/wp-github-updater.svg?style=flat-square)](https://packagist.org/packages/silverassist/wp-github-updater)

A reusable WordPress plugin updater that handles automatic updates from public GitHub releases. Perfect for WordPress plugins distributed outside the official repository.
A reusable WordPress plugin updater that handles automatic updates from GitHub releases, public or private. Perfect for WordPress plugins distributed outside the official repository.

## Features

Expand All @@ -18,6 +18,7 @@ A reusable WordPress plugin updater that handles automatic updates from public G
- 🗂️ **Enhanced File Handling**: Multi-tier temporary file creation to resolve hosting issues
- ✅ **Manual Version Checks**: AJAX-powered manual update checking with immediate admin feedback
- 🎨 **Built-in JavaScript** (v1.3.0+): Complete update check UI with no custom code needed
- 🔒 **Private repositories** (v1.4.0+): Reads releases from a private GitHub repository with a token

## Installation

Expand Down Expand Up @@ -129,13 +130,14 @@ $updater = new Updater($config);
| `plugin_author` | string | From plugin header | Plugin author name |
| `plugin_homepage` | string | GitHub repo URL | Plugin homepage URL |
| `requires_wordpress` | string | `'6.0'` | Minimum WordPress version |
| `requires_php` | string | `'8.3'` | Minimum PHP version |
| `requires_php` | string | `'8.2'` | Minimum PHP version |
| `asset_pattern` | string | `'{slug}-v{version}.zip'` | GitHub release asset filename pattern |
| `cache_duration` | int | `43200` (12 hours) | Cache duration in seconds |
| `ajax_action` | string | `'check_plugin_version'` | AJAX action name for manual checks |
| `ajax_nonce` | string | `'plugin_version_check'` | AJAX nonce name |
| `text_domain` | string | `'wp-github-updater'` | WordPress text domain for i18n **(New in 1.1.0)** |
| `custom_temp_dir` | string\|null | `null` | Custom temporary directory path **(New in 1.1.3)** |
| `token_constant` | string | `'SILVER_GITHUB_TOKEN'` | Name of the PHP constant or environment variable that holds the GitHub token for private repositories **(New in 1.4.0)** |

### Internationalization Support (i18n)

Expand Down Expand Up @@ -164,6 +166,68 @@ If your plugin slug is `my-awesome-plugin` and version is `1.2.3`:
- Default pattern: `my-awesome-plugin-v1.2.3.zip`
- Custom pattern: `my-plugin-1.2.3.zip` (using `{slug}-{version}.zip`)

## Private Repositories

Since 1.4.0 the updater can read the releases of a **private** GitHub repository. Public
repositories keep working exactly as before, with no token.

### Configure the token

The token is read from a PHP constant first, then from an environment variable, both named by the
`token_constant` option (default `SILVER_GITHUB_TOKEN`). It is never read from the database or from
a settings screen.

```php
// wp-config.php
define( 'SILVER_GITHUB_TOKEN', 'the-token' );
```

Or set `SILVER_GITHUB_TOKEN` in the environment of the PHP process. If your PHP setup does not pass
environment variables to its workers (for example PHP-FPM with `clear_env`), define the constant in
`wp-config.php` from your secret store instead.

The token needs read access to the repository's contents and releases. For a classic personal
access token that is the `repo` scope. Never commit the token to a repository.

To use another name, set the option:

```php
$config = new UpdaterConfig( $pluginFile, 'owner/private-repo', [
'token_constant' => 'MY_PLUGIN_GITHUB_TOKEN',
] );
```

### How it works

- The token is sent as `Authorization: Bearer` to `api.github.com` only, never to any other host.
- A private release asset is downloaded through its API URL. GitHub answers with a redirect to a
signed storage URL, which the updater follows **without** the token.
- The release must have a ZIP asset attached, as for public repositories.
- A failed lookup is not cached, so the next check asks again.

### Troubleshooting

When a site stops seeing updates, the PHP error log says why. The token is never written to it.

| Log message | Meaning |
|-------------|---------|
| `GitHub rejected the token (HTTP 401)` | The token is invalid or expired. Replace it |
| `HTTP 403: the token has no access to the repository, or the rate limit was reached` | The token cannot read the repository, or GitHub is rate limiting it |
| `HTTP 403: the rate limit was reached or the repository is private. Define SILVER_GITHUB_TOKEN to authenticate` | No token is configured and GitHub refused the anonymous request |
| `HTTP 404: the release does not exist, or the token has no access to the repository` | Check the repository name, that the release exists, and the token's access |
| `HTTP 404: not found. If the repository is private, define SILVER_GITHUB_TOKEN` | The repository is private and no token is configured |

### Testing against a private repository

The behaviour tests run on the WordPress Test Suite and intercept requests with the
`pre_http_request` filter, so they never reach the network. An opt-in live test checks the real
GitHub API against a private repository that has a release with a ZIP asset:

```bash
WPGU_LIVE_REPO=owner/private-repo WPGU_LIVE_VERSION=1.2.3 SILVER_GITHUB_TOKEN=... \
vendor/bin/phpunit --testsuite wordpress --group external-http
```

## Manual Version Check

The updater provides AJAX endpoints for manual version checking. Starting with **version 1.3.0**, the package includes a built-in JavaScript solution that eliminates the need for custom scripts in consuming plugins.
Expand Down Expand Up @@ -389,7 +453,7 @@ The updater automatically tries multiple strategies for temporary file creation:
- PHP 8.2 or higher
- WordPress 6.0 or higher
- Composer for dependency management
- Public GitHub repository with releases
- GitHub repository with releases (public, or private with a token, see Private Repositories)

## Development

Expand Down
2 changes: 1 addition & 1 deletion assets/js/check-updates.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Receives configuration via wp_localize_script.
*
* @package SilverAssist\WpGithubUpdater
* @version 1.3.1
* @version 1.4.0
* @since 1.3.0
*/
(($) => {
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "silverassist/wp-github-updater",
"description": "A reusable WordPress plugin updater that handles automatic updates from public GitHub releases",
"description": "A reusable WordPress plugin updater that handles automatic updates from GitHub releases, public or private",
"type": "library",
"keywords": [
"wordpress",
Expand Down Expand Up @@ -55,7 +55,7 @@
"test:wordpress": "phpunit --testsuite wordpress",
"phpcs": "phpcs",
"phpcbf": "phpcbf",
"phpstan": "phpstan analyse src/ --level=8",
"phpstan": "phpstan analyse src/ --level=8 --memory-limit=512M",
"check": [
"@phpcs",
"@phpstan",
Expand Down
20 changes: 19 additions & 1 deletion examples/integration-guide.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,30 @@
* Installation steps for existing plugins:
*
* 1. Navigate to your plugin directory
* 2. Run: composer require silverassist/wp-github-updater
* 2. Declare the vcs repository (see the README) and run: composer require silverassist/wp-github-updater
* 3. Replace your existing updater code with the examples above
* 4. Remove your old updater class files
* 5. Test the updates
*/

/**
* Private repositories (v1.4.0+):
*
* To update a plugin from a private GitHub repository, give the site a token that can read it.
* The updater reads it from a PHP constant, then from an environment variable, never from the
* database. It is sent to api.github.com only.
*/

/*
// wp-config.php
define('SILVER_GITHUB_TOKEN', 'the-token');

// Optional: use another name for the constant or environment variable.
$config = new UpdaterConfig($pluginFile, 'owner/private-repo', [
'token_constant' => 'MY_PLUGIN_GITHUB_TOKEN',
]);
*/

/**
* New Features in v1.3.0:
*
Expand Down
3 changes: 0 additions & 3 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,3 @@ parameters:
- vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
scanDirectories:
- vendor/php-stubs/wordpress-stubs
ignoreErrors:
# Ignore WP_Error union type issues in some contexts
- '#Cannot call method get_error_message\(\) on string\|WP_Error\|false#'
Loading
Loading