diff --git a/.gitattributes b/.gitattributes
index 3344d2b..7b61404 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -7,4 +7,5 @@
/phpcs.xml export-ignore
/phpstan.neon export-ignore
/tests export-ignore
-/CHANGELOG.md export-ignore
+/scripts export-ignore
+/composer.lock export-ignore
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index c7818df..2df6464 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1,30 +1,431 @@
-
+# Copilot Instructions for WP GitHub Updater Package
-- [x] Verify that the copilot-instructions.md file in the .github directory is created.
+## 📋 Documentation Policy
-- [x] Clarify Project Requirements
-
+**CRITICAL: ALL documentation must be consolidated in these three files ONLY:**
-- [x] Scaffold the Project
-
+1. **README.md** - User-facing documentation, installation, usage examples, API reference
+2. **CHANGELOG.md** - Version history, changes, bug fixes, new features
+3. **.github/copilot-instructions.md** - This file, development guidelines and architecture
-- [x] Customize the Project
-
+**🚫 NEVER create separate documentation files:**
+- ❌ Do NOT create files like `docs/GUIDE.md`, `docs/TESTING.md`, `TROUBLESHOOTING.md`, etc.
+- ❌ Do NOT create a `docs/` directory
+- ❌ Do NOT split documentation into multiple MD files
-- [x] Install Required Extensions
-
+**✅ Instead:**
+- Add user documentation to appropriate sections in README.md
+- Add development/architecture notes to this copilot-instructions.md file
+- Add version changes to CHANGELOG.md
+- Use inline code comments for complex logic explanation
-- [x] Compile the Project
-
+**Rationale:** Centralized documentation is easier to maintain, search, and keep in sync with code changes.
-- [x] Create and Run Task
-
+---
-- [x] Launch the Project
-
+## Architecture Overview
-- [x] Ensure Documentation is Complete
-
+This is a **reusable Composer package** that provides WordPress plugin update functionality from GitHub releases. The package is designed to be integrated into various WordPress plugins, not as a standalone plugin.
-## Project: WordPress GitHub Updater Composer Package
-✅ **COMPLETED** - A reusable Composer package that handles automatic updates from public GitHub releases for WordPress plugins.
+## Core Technologies
+
+- **PHP 8.2+**: Modern PHP with strict typing, union types, and nullable parameters
+- **WordPress 6.0+**: WordPress update system integration via hooks and filters
+- **PSR-4 Autoloading**: Namespace-based class loading for better organization
+- **GitHub API v3**: REST API integration for release management
+- **Composer Package**: Distributed via Packagist as `silverassist/wp-github-updater`
+
+## 🔒 PHPUnit Version Policy - CRITICAL
+
+**MANDATORY: PHPUnit MUST remain at version 9.6.x**
+
+### Rationale
+- **WordPress Ecosystem Standard**: PHPUnit 9.6 is the most widely used version across WordPress projects
+- **WordPress Coding Standards**: The official [WordPress Coding Standards](https://github.com/WordPress/WordPress-Coding-Standards) uses PHPUnit 9.x
+- **Yoast PHPUnit Polyfills**: Version 4.x supports PHPUnit 7.5-9.x, 11.x, and 12.x, but **NOT 10.x**
+- **Consumer Compatibility**: Most projects depending on this package use PHPUnit 9.6
+
+### Version Constraints
+```json
+{
+ "require-dev": {
+ "phpunit/phpunit": "^9.6",
+ "yoast/phpunit-polyfills": "^4.0"
+ }
+}
+```
+
+### What NOT to do
+- ❌ Do NOT upgrade PHPUnit to version 10.x (not supported by Yoast PHPUnit Polyfills)
+- ❌ Do NOT upgrade to PHPUnit 11.x or 12.x (breaks compatibility with most WordPress projects)
+- ❌ Do NOT accept Dependabot PRs that upgrade PHPUnit beyond 9.x
+
+### Configuration Files
+- **dependabot.yml**: Configured to ignore PHPUnit major version updates
+- **composer.json**: Locked to `^9.6` version constraint
+- **phpunit.xml**: Uses PHPUnit 9.6 schema reference
+
+### When to Reconsider
+Only upgrade PHPUnit when:
+1. WordPress Coding Standards officially adopts a newer version
+2. Majority of WordPress ecosystem projects have migrated
+3. All dependent projects confirm compatibility
+
+## Project Structure
+
+```
+wp-github-updater/
+├── .github/
+│ └── copilot-instructions.md
+├── src/
+│ ├── Updater.php # Main updater class
+│ └── UpdaterConfig.php # Configuration management
+├── tests/
+│ └── UpdaterConfigTest.php # Unit tests
+├── examples/
+│ └── integration-guide.php # Integration examples
+├── composer.json # Package definition
+├── README.md # Documentation
+├── CHANGELOG.md # Version history
+├── LICENSE.md # PolyForm Noncommercial 1.0.0 license
+├── phpcs.xml # Code standards
+├── phpstan.neon # Static analysis
+└── phpunit.xml # Test configuration
+```
+
+## Component Architecture
+
+### UpdaterConfig Class
+**Purpose**: Configuration management for the updater functionality
+**Key Responsibilities**:
+- Store plugin metadata (name, version, file path, etc.)
+- GitHub repository configuration
+- Cache duration and WordPress requirements
+- AJAX endpoints and nonce configuration
+- Plugin data parsing from WordPress headers
+- **Custom temporary directory configuration** (v1.1.3+)
+
+### Updater Class
+**Purpose**: Core WordPress integration and GitHub API communication
+**Key Responsibilities**:
+- WordPress update system hooks integration
+- GitHub API communication with caching
+- Plugin update checking and information display
+- Download error handling and recovery
+- AJAX endpoints for manual update checks and notice dismissal
+- Markdown-to-HTML conversion for changelogs
+- **Enhanced temporary file management with PCLZIP error resolution** (v1.1.3+)
+- **WordPress admin notices for manual update checks** (v1.1.3+)
+- **Dismissible admin notices with AJAX handling** (v1.1.4+)
+
+### Security
+- **Nonce verification** for AJAX requests
+- **Capability checks** for update operations
+- **Input sanitization** using WordPress functions
+- **Output escaping** for all displayed content
+- **WordPress transients** for secure caching
+
+## Configuration & Settings
+
+### Internationalization Strategy
+Since this is a **reusable Composer package**, internationalization should be handled by the **consuming plugin**, not the package itself. The package should:
+
+1. **NO hardcoded text domain** - Allow consuming plugin to pass text domain
+2. **Accept text domain parameter** in UpdaterConfig constructor
+3. **Pass text domain to all i18n functions** from consuming plugin
+4. **Provide translation function wrappers** that use the passed text domain
+
+```php
+// ✅ CORRECT - Package approach
+class UpdaterConfig {
+ private string $textDomain;
+
+ public function __construct(array $options) {
+ $this->textDomain = $options["text_domain"] ?? "wp-github-updater";
+ }
+
+ private function translate(string $text): string {
+ return \__($text, $this->textDomain);
+ }
+}
+
+// ❌ INCORRECT - Fixed text domain in package
+\__("Update available", "wp-github-updater");
+```
+
+### Temporary File Management Strategy
+**New in v1.1.3**: Enhanced handling to resolve `PCLZIP_ERR_MISSING_FILE (-4)` errors
+
+#### Multi-Tier Fallback System
+The package implements a robust 6-tier fallback system for temporary file creation:
+
+1. **Custom temporary directory** - User-specified via `custom_temp_dir` option
+2. **WordPress uploads directory** - Most reliable, within web root
+3. **WP_CONTENT_DIR/temp** - Auto-created if needed
+4. **WP_TEMP_DIR** - If defined in wp-config.php
+5. **System temporary directory** - OS default /tmp or equivalent
+6. **Manual file creation** - Last resort fallback
+
+#### Configuration Options
+```php
+$config = new UpdaterConfig($pluginFile, $githubRepo, [
+ "custom_temp_dir" => WP_CONTENT_DIR . "/temp", // Custom directory
+ // or
+ "custom_temp_dir" => wp_upload_dir()["basedir"] . "/temp", // Uploads subdirectory
+]);
+```
+
+#### WordPress Configuration Alternative
+```php
+// In wp-config.php
+define('WP_TEMP_DIR', ABSPATH . 'wp-content/temp');
+```
+
+### Coding Standards
+
+- **WordPress Coding Standards**: Full compliance with WordPress PHP standards
+- **PSR-4 Autoloading**: Proper namespace organization (`SilverAssist\WpGithubUpdater`)
+- **Type Declarations**: Use PHP 8+ strict typing everywhere
+- **Error Handling**: Comprehensive error handling with WordPress WP_Error
+- **Security First**: Nonce verification and capability checks
+
+### WordPress Integration
+
+- **Hooks & Filters**: Use appropriate WordPress hooks for update functionality
+- **Transient Caching**: Leverage WordPress transients for GitHub API caching
+- **Plugin API**: Integrate with WordPress plugin update system
+- **HTTP API**: Use wp_remote_get() for GitHub API communication
+- **File System**: Use WordPress file system functions for downloads
+
+### Security Best Practices
+
+- **Input Validation**: Sanitize all user inputs using WordPress functions
+- **Output Escaping**: Escape all outputs using appropriate WordPress functions
+- **Nonce Verification**: Use WordPress nonces for AJAX security
+- **Capability Checks**: Verify user permissions before update operations
+- **HTTP Security**: Proper User-Agent headers and timeout handling
+
+### Release Process
+
+- **Version Bumping**: Update version in composer.json and CHANGELOG.md
+- **Git Tagging**: Create semantic version tags (v1.0.0, v1.1.0, etc.)
+- **Documentation**: Update README and integration examples
+- **Packagist**: Automatic distribution via Packagist on tag push
+
+### Core Structure
+
+```php
+namespace SilverAssist\WpGithubUpdater;
+
+class Updater {
+ private UpdaterConfig $config;
+ private string $pluginSlug;
+ private string $currentVersion;
+ // ... other properties
+
+ public function __construct(UpdaterConfig $config) {
+ // Initialize updater with configuration
+ }
+
+ public function checkForUpdate($transient) {
+ // WordPress update check integration using isUpdateAvailable() method
+ }
+
+ public function pluginInfo($result, string $action, object $args) {
+ // Plugin information display
+ }
+
+ public function manualVersionCheck(): void {
+ // AJAX handler for manual version checks with admin notice creation
+ }
+
+ public function dismissUpdateNotice(): void {
+ // AJAX handler for dismissing admin notices (v1.1.4+)
+ }
+
+ public function showUpdateNotice(): void {
+ // WordPress admin notice display for available updates (v1.1.4+)
+ }
+
+ public function maybeFixDownload($result, string $package, object $upgrader, array $hook_extra) {
+ // Enhanced download handling with PCLZIP error resolution (v1.1.3+)
+ }
+
+ private function createSecureTempFile(string $package) {
+ // Multi-tier temporary file creation with fallback strategies (v1.1.3+)
+ }
+
+ public function isUpdateAvailable(): bool {
+ // Centralized update availability check (refactored in v1.1.4+)
+ }
+
+ public function getLatestVersion(): string|false {
+ // Public API method for external access (v1.1.2+)
+ }
+
+ // ... other methods
+}
+```
+
+## 🚨 CRITICAL CODING STANDARDS - MANDATORY COMPLIANCE
+
+### String Quotation Standards
+- **MANDATORY**: ALL strings in PHP MUST use double quotes: `"string"`
+- **i18n Functions**: ALL WordPress i18n functions MUST use double quotes: `__("Text", $textDomain)`
+- **FORBIDDEN**: Single quotes for strings: `'string'` or `__('text', 'domain')`
+- **Exception**: Only use single quotes inside double-quoted strings when necessary
+- **SQL Queries**: Use double quotes for string literals in SQL: `WHERE option_value = "1"`
+
+### Documentation Requirements
+- **PHP**: Complete PHPDoc documentation for ALL classes, methods, and properties
+- **@since tags**: Required for all public APIs with version numbers
+- **English only**: All documentation must be in English for international collaboration
+- **Package context**: Document as library package, not standalone plugin
+
+### WordPress i18n Standards for Packages
+- **NO fixed text domain**: Accept text domain from consuming plugin
+- **Translation wrapper methods**: Provide internal translation methods
+- **Consuming plugin responsibility**: Let the consuming plugin handle actual translations
+- **Flexible configuration**: Allow text domain to be passed in configuration
+
+#### Text Domain Configuration Example
+```php
+class UpdaterConfig {
+ private string $textDomain;
+
+ public function __construct(array $options) {
+ $this->textDomain = $options["text_domain"] ?? "wp-github-updater";
+ }
+
+ private function __($text): string {
+ return \__($text, $this->textDomain);
+ }
+
+ private function esc_html__($text): string {
+ return \esc_html__($text, $this->textDomain);
+ }
+}
+```
+
+## Modern PHP 8+ Conventions
+
+### Type Declarations
+- **Strict typing**: All methods use parameter and return type declarations
+- **Nullable types**: Use `?Type` for optional returns (e.g., `?string`)
+- **Property types**: All class properties have explicit types
+- **Union types**: Use `string|false` for methods that can return string or false
+
+### PHP Coding Standards
+- **Double quotes for all strings**: `"string"` not `'string'` - MANDATORY
+- **String interpolation**: Use `"prefix_{$variable}"` instead of concatenation
+- **Short array syntax**: `[]` not `array()`
+- **Namespaces**: Use descriptive namespace `SilverAssist\WpGithubUpdater`
+- **WordPress hooks**: `\add_action("init", [$this, "method"])` with array callbacks
+- **PHP 8+ Features**: Match expressions, array spread operator, typed properties
+
+### Function Prefix Usage - MANDATORY COMPLIANCE
+
+**🚨 CRITICAL RULE: Use `\` prefix for ALL WordPress functions in namespaced context, but NOT for PHP native functions**
+
+```php
+// ✅ CORRECT - WordPress functions REQUIRE \ prefix in namespaced context
+\add_action("init", [$this, "method"]);
+\add_filter("pre_set_site_transient_update_plugins", [$this, "checkForUpdate"]);
+\get_option("option_name", "default");
+\wp_remote_get($url, $args);
+\get_transient($key);
+\set_transient($key, $value, $expiration);
+\plugin_basename($file);
+\get_plugin_data($file);
+
+// ✅ CORRECT - WordPress i18n functions REQUIRE \ prefix
+\__("Text to translate", $this->textDomain);
+\esc_html__("Text to translate", $this->textDomain);
+\wp_kses_post($content);
+
+// ✅ CORRECT - PHP native functions do NOT need \ prefix
+array_key_exists($key, $array);
+json_decode($response, true);
+version_compare($version1, $version2, "<");
+preg_replace("/pattern/", "replacement", $string);
+basename($path);
+fwrite($handle, $data);
+
+// ❌ INCORRECT - Missing \ prefix for WordPress functions
+add_action("init", [$this, "method"]);
+get_transient($key);
+wp_remote_get($url);
+
+// ❌ INCORRECT - Don't use \ with PHP native functions
+\json_decode($response);
+\version_compare($v1, $v2);
+\basename($path);
+```
+
+### Package-Specific Function Categories
+
+#### **WordPress Functions (ALL need `\` prefix):**
+- **Update System**: `\add_filter("pre_set_site_transient_update_plugins")`, `\add_filter("plugins_api")`
+- **Plugin Functions**: `\plugin_basename()`, `\get_plugin_data()`, `\plugin_dir_path()`
+- **HTTP API**: `\wp_remote_get()`, `\wp_remote_retrieve_body()`, `\is_wp_error()`
+- **Transients**: `\get_transient()`, `\set_transient()`, `\delete_transient()`
+- **Security**: `\wp_verify_nonce()`, `\current_user_can()`, `\wp_kses_post()`
+- **File System**: `\wp_upload_dir()`, `\wp_tempnam()`, `\wp_filesystem()`
+
+#### **PHP Native Functions (NO `\` prefix needed):**
+- **Array Functions**: `array_key_exists()`, `array_merge()`, `count()`
+- **String Functions**: `basename()`, `dirname()`, `pathinfo()`, `trim()`
+- **JSON Functions**: `json_decode()`, `json_encode()`
+- **Version Functions**: `version_compare()`
+- **File Functions**: `fopen()`, `fwrite()`, `fclose()`
+- **Regex Functions**: `preg_replace()`, `preg_match()`
+
+## Development Workflow
+
+### Integration Pattern
+The package is designed to be integrated into WordPress plugins like this:
+
+```php
+// In the consuming plugin
+use SilverAssist\WpGithubUpdater\UpdaterConfig;
+use SilverAssist\WpGithubUpdater\Updater;
+
+$config = new UpdaterConfig([
+ "plugin_file" => __FILE__,
+ "plugin_slug" => "my-plugin/my-plugin.php",
+ "github_username" => "username",
+ "github_repo" => "repository",
+ "text_domain" => "my-plugin-domain", // Consumer's text domain
+ "custom_temp_dir" => WP_CONTENT_DIR . "/temp", // Optional: Custom temp directory (v1.1.3+)
+ // ... other options
+]);
+
+$updater = new Updater($config);
+```
+
+### Error Handling Strategy
+- Use WordPress `WP_Error` class for error management
+- Provide fallback download mechanisms for GitHub API failures
+- Cache GitHub API responses to avoid rate limiting
+- Handle PCLZIP errors with alternative download methods
+- **PCLZIP_ERR_MISSING_FILE Resolution** (v1.1.3+): Multi-tier temporary file creation to avoid /tmp permission issues
+- Cache GitHub API responses to avoid rate limiting
+- Handle PCLZIP errors with alternative download methods
+
+### Version Management
+- Follow semantic versioning (MAJOR.MINOR.PATCH)
+- Update composer.json version field
+- Create git tags for each release
+- Update CHANGELOG.md with detailed release notes
+- Maintain backward compatibility within major versions
+
+## Translation Support Strategy
+
+Since this is a **reusable package**, translation should be handled by the **consuming plugin**:
+
+1. **Package provides**: English text strings and translation function wrappers
+2. **Consuming plugin provides**: Text domain and handles actual translation loading
+3. **Configuration based**: Text domain passed via UpdaterConfig constructor
+4. **Fallback domain**: Default to "wp-github-updater" if no text domain provided
+
+This approach ensures the package can be integrated into any plugin while respecting the consuming plugin's translation strategy and text domain conventions.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..d9a5bbd
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,50 @@
+version: 2
+updates:
+ # Composer dependency updates
+ - package-ecosystem: "composer"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "monday"
+ time: "09:00"
+ timezone: "America/Mexico_City"
+ labels:
+ - "dependencies"
+ - "composer"
+ - "automated"
+ open-pull-requests-limit: 10
+ # Ignore PHPUnit major version updates
+ # Keep at 9.x for WordPress compatibility
+ ignore:
+ - dependency-name: "phpunit/phpunit"
+ update-types: ["version-update:semver-major"]
+ groups:
+ composer-updates:
+ patterns:
+ - "*"
+ update-types:
+ - "minor"
+ - "patch"
+
+ # GitHub Actions dependency updates
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "monday"
+ time: "09:00"
+ timezone: "America/Mexico_City"
+ labels:
+ - "dependencies"
+ - "github-actions"
+ - "automated"
+ open-pull-requests-limit: 10
+
+ # Group all GitHub Actions updates together
+ groups:
+ github-actions-updates:
+ patterns:
+ - "*"
+ update-types:
+ - "minor"
+ - "patch"
diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml
new file mode 100644
index 0000000..63a3a81
--- /dev/null
+++ b/.github/workflows/create-release.yml
@@ -0,0 +1,314 @@
+name: Create Release
+
+on:
+ push:
+ tags:
+ - "v*"
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version number (e.g., 1.1.0)"
+ required: true
+ default: "1.1.0"
+
+jobs:
+ build-and-release:
+ permissions:
+ contents: write
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # v2.35.5
+ with:
+ php-version: "8.2"
+ extensions: mbstring, xml, ctype, json, tokenizer, mysqli
+ coverage: none
+
+ - 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
+ echo "✅ MySQL database configured"
+
+ - name: Install Subversion (required for WordPress Test Suite)
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y subversion
+ echo "✅ Subversion installed"
+
+ - name: Install Composer dependencies
+ run: |
+ composer install --optimize-autoloader --no-interaction
+ echo "✅ Composer dependencies installed successfully (including dev dependencies for testing)"
+
+ - name: Install WordPress Test Suite
+ run: |
+ echo "📦 Installing WordPress Test Suite..."
+ chmod +x scripts/install-wp-tests.sh
+ bash scripts/install-wp-tests.sh wordpress_test wp_test wp_test localhost latest
+ echo "✅ WordPress Test Suite installed"
+
+ - name: Extract version from tag or input
+ id: version
+ run: |
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ VERSION="${{ github.event.inputs.version }}"
+ else
+ VERSION=${GITHUB_REF#refs/tags/v}
+ fi
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "tag=v$VERSION" >> $GITHUB_OUTPUT
+
+ - name: Validate package structure
+ run: |
+ VERSION="${{ steps.version.outputs.version }}"
+
+ echo "🔍 Validating WP GitHub Updater package structure..."
+
+ # Check core files exist
+ if [ ! -f "src/Updater.php" ]; then
+ echo "❌ Missing core file: src/Updater.php"
+ exit 1
+ fi
+
+ if [ ! -f "src/UpdaterConfig.php" ]; then
+ echo "❌ Missing core file: src/UpdaterConfig.php"
+ exit 1
+ fi
+
+ if [ ! -f "composer.json" ]; then
+ echo "❌ Missing composer.json"
+ exit 1
+ fi
+
+ # Verify version consistency in composer.json
+ COMPOSER_VERSION=$(grep '"version"' composer.json | head -n1 | sed 's/.*"version": *"\([^"]*\)".*/\1/')
+ if [ "$COMPOSER_VERSION" != "$VERSION" ]; then
+ echo "⚠️ Warning: Version in composer.json ($COMPOSER_VERSION) doesn't match tag ($VERSION)"
+ else
+ echo "✅ Version consistency verified: $VERSION"
+ fi
+
+ echo "✅ Package structure validation completed"
+
+ - name: Run tests
+ run: |
+ echo "🧪 Running PHPUnit tests with WordPress Test Suite..."
+ echo "Working directory: $(pwd)"
+ echo "Checking for phpunit.xml:"
+ ls -la phpunit.xml || echo "phpunit.xml not found"
+ echo "Checking tests directory:"
+ ls -la tests/
+ ./vendor/bin/phpunit --configuration=phpunit.xml
+ echo "✅ All tests passed (including WordPress integration tests)"
+
+ - name: Run code quality checks
+ run: |
+ echo "🔍 Running PHP CodeSniffer..."
+ ./vendor/bin/phpcs --standard=phpcs.xml src/ tests/
+ echo "✅ Code quality checks passed"
+
+ - name: Generate release notes from CHANGELOG
+ id: changelog
+ run: |
+ VERSION="${{ steps.version.outputs.version }}"
+
+ echo "📝 Generating release notes from CHANGELOG.md..."
+
+ # Extract current version changes from CHANGELOG.md
+ if [ -f "CHANGELOG.md" ]; then
+ echo "Looking for version $VERSION in CHANGELOG.md"
+
+ # Method 1: Using sed with escaped brackets (most reliable)
+ CHANGES=$(sed -n "/## \\[${VERSION}\\]/,/## \\[/p" CHANGELOG.md | sed '$d' | tail -n +2)
+
+ if [ -n "$CHANGES" ]; then
+ echo "Found changes with sed method"
+ echo "## What's New in v$VERSION" > release_notes.md
+ echo "$CHANGES" >> release_notes.md
+ else
+ echo "Sed method failed, trying awk method"
+ # Method 2: Using awk (more reliable across different systems)
+ CHANGES_AWK=$(awk "/^## \\[${VERSION}\\]/{flag=1; next} /^## \\[/{flag=0} flag" CHANGELOG.md)
+
+ if [ -n "$CHANGES_AWK" ]; then
+ echo "Found changes with awk method"
+ echo "## What's New in v$VERSION" > release_notes.md
+ echo "$CHANGES_AWK" >> release_notes.md
+ else
+ echo "Using fallback release notes"
+ echo "## What's New in v$VERSION" > release_notes.md
+ echo "See [CHANGELOG.md](CHANGELOG.md) for detailed changes." >> release_notes.md
+ fi
+ fi
+ else
+ echo "CHANGELOG.md not found, creating default release notes"
+ echo "## Release v$VERSION" > release_notes.md
+ echo "New release of WP GitHub Updater package" >> release_notes.md
+ fi
+
+ # Add package information to release notes
+ cat >> release_notes.md << EOF
+
+ ## 📦 Package Information
+ - **Package Name**: wp-github-updater
+ - **Version**: $VERSION
+ - **Namespace**: SilverAssist\\WpGithubUpdater
+ - **License**: PolyForm Noncommercial 1.0.0
+ - **PHP Version**: 8.2+
+ - **WordPress Version**: 6.0+
+
+ ## 🚀 Installation via Composer
+ \`\`\`bash
+ composer require silverassist/wp-github-updater:^$VERSION
+ \`\`\`
+
+ ## 📋 Basic Usage
+ \`\`\`php
+ use SilverAssist\\WpGithubUpdater\\UpdaterConfig;
+ use SilverAssist\\WpGithubUpdater\\Updater;
+
+ // Configure the updater
+ \$config = new UpdaterConfig(__FILE__, 'owner/repository', [
+ 'text_domain' => 'my-plugin-textdomain', // v1.1.0+ feature
+ ]);
+
+ // Initialize the updater
+ \$updater = new Updater(\$config);
+ \`\`\`
+
+ ## ✨ Key Features
+ - 🔄 Automatic WordPress plugin updates from GitHub releases
+ - 🌍 Configurable internationalization (i18n) support
+ - 📋 WordPress admin integration with update notifications
+ - 🔒 Secure GitHub API communication with proper error handling
+ - ⚡ Efficient caching system for version checks
+ - 🧪 Comprehensive PHPUnit test suite
+ - 📝 PSR-12 + WordPress coding standards compliance
+ - 🎯 Modern PHP 8+ architecture with strict typing
+
+ ## 📚 Documentation
+ - **README**: [Installation and usage guide](README.md)
+ - **CHANGELOG**: [Complete version history](CHANGELOG.md)
+ - **Examples**: [Integration examples](examples/)
+ - **API Docs**: Comprehensive PHPDoc documentation
+
+ ## 🔧 Requirements
+ - PHP 8.2 or higher
+ - WordPress 6.0 or higher
+ - Composer for package management
+ - GitHub repository with releases for updates
+
+ ## 🐛 Issues & Support
+ Found a bug or need help? Please [open an issue](https://github.com/SilverAssist/wp-github-updater/issues) on GitHub.
+ EOF
+
+ - name: Update RELEASE-NOTES.md
+ run: |
+ VERSION="${{ steps.version.outputs.version }}"
+ DATE=$(date +"%B %d, %Y")
+
+ # Create updated RELEASE-NOTES.md
+ cat > RELEASE-NOTES.md << EOF
+ # WP GitHub Updater - Release v$VERSION
+
+ ## Package Information
+ - **Package Name**: wp-github-updater
+ - **Version**: $VERSION
+ - **Release Date**: $DATE
+ - **License**: PolyForm Noncommercial 1.0.0
+ - **Repository**: https://github.com/SilverAssist/wp-github-updater
+
+ ## Package Contents
+ - Core updater classes (\`src/Updater.php\`, \`src/UpdaterConfig.php\`)
+ - Comprehensive test suite (\`tests/\`)
+ - Integration examples (\`examples/\`)
+ - Documentation ([README.md](README.md), [CHANGELOG.md](CHANGELOG.md))
+ - Coding standards configuration (\`phpcs.xml\`)
+ - PHPUnit configuration (\`phpunit.xml\`)
+ - Composer package definition (\`composer.json\`)
+
+ ## Installation via Composer
+ \`\`\`bash
+ composer require silverassist/wp-github-updater:^$VERSION
+ \`\`\`
+
+ ## Requirements
+ - PHP 8.2+
+ - WordPress 6.0+
+ - Composer
+ - GitHub repository with releases
+
+ ## Key Features
+ - Automatic WordPress plugin updates from GitHub releases
+ - Configurable internationalization (i18n) support
+ - WordPress admin dashboard integration
+ - Secure GitHub API communication
+ - Efficient version caching system
+ - Modern PHP 8+ architecture with strict typing
+ - Comprehensive test coverage
+ - PSR-12 + WordPress coding standards
+
+ ## Usage Example
+ \`\`\`php
+ use SilverAssist\\WpGithubUpdater\\UpdaterConfig;
+ use SilverAssist\\WpGithubUpdater\\Updater;
+
+ // Configure the updater
+ \$config = new UpdaterConfig(__FILE__, 'owner/repository', [
+ 'text_domain' => 'my-plugin-textdomain',
+ ]);
+
+ // Initialize the updater
+ \$updater = new Updater(\$config);
+ \`\`\`
+
+ ## Support & Documentation
+ - **Installation Guide**: [README.md](README.md)
+ - **Change History**: [CHANGELOG.md](CHANGELOG.md)
+ - **Integration Examples**: [examples/](examples/)
+ - **Issues**: [GitHub Issues](https://github.com/SilverAssist/wp-github-updater/issues)
+
+ ## Distribution
+ - **Packagist**: https://packagist.org/packages/silverassist/wp-github-updater
+ - **GitHub Releases**: https://github.com/SilverAssist/wp-github-updater/releases
+ EOF
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
+ with:
+ tag_name: ${{ steps.version.outputs.tag }}
+ name: "WP GitHub Updater v${{ steps.version.outputs.version }}"
+ body_path: release_notes.md
+ draft: false
+ prerelease: false
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Summary
+ run: |
+ echo "## 🚀 Release Summary" >> $GITHUB_STEP_SUMMARY
+ echo "- **Package**: WP GitHub Updater" >> $GITHUB_STEP_SUMMARY
+ echo "- **Version**: ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Composer**: \`silverassist/wp-github-updater:^${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
+ echo "- **Release**: [View Release](https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.tag }})" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "### ✨ Package Features" >> $GITHUB_STEP_SUMMARY
+ echo "- 🔄 Automatic WordPress plugin updates from GitHub" >> $GITHUB_STEP_SUMMARY
+ echo "- 🌍 Configurable internationalization support" >> $GITHUB_STEP_SUMMARY
+ echo "- 📋 WordPress admin dashboard integration" >> $GITHUB_STEP_SUMMARY
+ echo "- 🔒 Secure GitHub API communication" >> $GITHUB_STEP_SUMMARY
+ echo "- ⚡ Efficient caching system" >> $GITHUB_STEP_SUMMARY
+ echo "- 🎯 Modern PHP 8+ architecture" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "### 📦 Installation" >> $GITHUB_STEP_SUMMARY
+ echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
+ echo "composer require silverassist/wp-github-updater:^${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
+ echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
diff --git a/.gitignore b/.gitignore
index 0d19fdb..50ebd19 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,7 @@
composer.lock
# PHPUnit
-/phpunit.xml
+# /phpunit.xml # Keep phpunit.xml for CI/CD workflow
/.phpunit.result.cache
# Code coverage
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 911c111..b576de1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,155 @@
# Changelog
+## [1.2.1] - 2025-10-11
+
+### Changed
+- **PHP Version Requirement**: Lowered minimum PHP version from 8.3 to 8.2 for broader compatibility
+- **Documentation Updates**: Updated all documentation, examples, and tests to reflect PHP 8.2 requirement
+- **Workflow Updates**: Updated GitHub Actions workflow to reflect PHP 8.2 requirement
+
+### Testing
+- **51 Tests Passing**: All tests verified with PHP 8.2 compatibility
+- **Test Fixtures Updated**: Updated all test fixtures and expectations for PHP 8.2
+
+## [1.2.0] - 2025-10-10
+
+### Changed
+- **Documentation Consolidation**: Centralized all documentation into README.md, CHANGELOG.md, and .github/copilot-instructions.md
+- **Removed Separate Documentation Files**: Eliminated `docs/` directory to maintain simpler, more maintainable documentation structure
+- **Testing Documentation**: Moved comprehensive testing guide to README.md Development section
+- **Troubleshooting Guide**: Integrated troubleshooting information directly into README.md
+
+### Testing
+- **51 Tests, 130 Assertions**: Complete test suite with 100% pass rate
+- **Real GitHub API Integration Tests**: 9 tests making actual HTTP requests to production repositories
+- **Test Coverage**: Unit tests (3), Integration tests (22), WordPress tests (26)
+- **Performance Verification**: Caching performance tests confirm < 10ms for cached API calls
+
+## [1.1.5] - 2025-10-10
+### Fixed
+- **PCLZIP_ERR_MISSING_FILE (-4) Resolution**: Complete rewrite of `upgrader_pre_download` filter to properly handle all download scenarios
+- **Download Filter Return Values**: Fixed critical issue where filter could return invalid types causing WordPress to fail with PCLZIP errors
+- **Better Plugin Detection**: Added robust verification to ensure filter only intercepts downloads for the correct plugin
+- **Enhanced Error Handling**: Comprehensive error messages for all failure points in the download process
+- **File Verification**: Added multiple validation checks (file size, readability, existence) before returning downloaded file to WordPress
+
+### Changed
+- **Stricter Filter Logic**: `maybeFixDownload()` now returns `false` to let WordPress handle downloads that aren't for our plugin
+- **Safety Checks**: Added verification of `hook_extra` data to ensure we only process downloads for our specific plugin
+- **Improved Documentation**: Enhanced PHPDoc comments explaining critical return value requirements for WordPress compatibility
+- **Download Process**: Better handling of HTTP response codes and empty responses with descriptive error messages
+
+### Technical Improvements
+- **Return Type Enforcement**: Strict enforcement of `string|WP_Error|false` return types (never `true` or other types)
+- **Multi-line Conditionals**: Improved code formatting to meet WordPress Coding Standards (120 character line limit)
+- **Defensive Programming**: Added early returns for edge cases where previous filters have already handled the download
+- **Minimum File Size Check**: Validates downloaded file is at least 100 bytes before considering it valid
+
+## [1.1.4] - 2025-08-29
+### Added
+- **WordPress Admin Notices**: Integrated admin notification system that displays update availability after manual version checks
+- **Dismissible Update Notices**: Users can dismiss update notifications with built-in AJAX functionality
+- **Admin Notice Management**: New `showUpdateNotice()` method creates WordPress-compliant admin notices with proper styling
+- **AJAX Notice Dismissal**: New `dismissUpdateNotice()` AJAX handler for seamless notice management
+- **Transient-Based Notifications**: Update notices persist for the same duration as version cache (configurable via `cache_duration`)
+
+### Changed
+- **Improved Manual Version Checks**: Enhanced `manualVersionCheck()` method now sets admin notices for immediate user feedback
+- **Code Refactoring**: Centralized update availability logic using `isUpdateAvailable()` method to eliminate code duplication
+- **Better WordPress Integration**: Manual version checks now properly clear WordPress update transients for immediate admin interface updates
+- **Enhanced User Experience**: Update checks provide both AJAX responses and persistent admin notifications
+
+### Fixed
+- **WordPress Admin Sync**: Manual version checks now immediately reflect in WordPress admin plugins page
+- **Transient Cache Management**: Proper clearing of both plugin-specific and WordPress update caches
+- **Admin Interface Updates**: Resolved disconnect between manual checks and WordPress admin display
+
+### Technical Improvements
+- **DRY Principle**: Replaced duplicate version comparison logic with centralized `isUpdateAvailable()` method calls
+- **AJAX Security**: Enhanced nonce verification and sanitization for all AJAX endpoints
+- **WordPress Standards**: All admin notices follow WordPress UI/UX guidelines with proper escaping and styling
+- **JavaScript Integration**: Inline JavaScript for notice dismissal with jQuery compatibility
+
+### Documentation
+- **API Documentation**: Added comprehensive Public API Methods section to README
+- **Integration Examples**: Updated all examples to demonstrate new admin notice features
+- **Configuration Guide**: Enhanced advanced configuration examples with new capabilities
+- **Code Examples**: Programmatic version checking examples for developers
+
+## [1.1.3] - 2025-08-29
+### Added
+- **Enhanced Temporary File Handling**: Implemented multiple fallback strategies for temporary file creation to resolve `PCLZIP_ERR_MISSING_FILE (-4)` errors
+- **Custom Temporary Directory Support**: New `custom_temp_dir` configuration option in UpdaterConfig for specifying alternative temporary directories
+- **Automatic Directory Creation**: The updater now attempts to create temporary directories if they don't exist
+- **Comprehensive File Verification**: Added file existence and readability checks after download to prevent installation failures
+
+### Changed
+- **Improved Download Reliability**: Enhanced `maybeFixDownload()` method with better error handling and multiple fallback strategies
+- **Robust Temporary File Strategy**: Six-tier fallback system for temporary file creation:
+ 1. Custom temporary directory (if configured)
+ 2. WordPress uploads directory
+ 3. WP_CONTENT_DIR/temp (auto-created)
+ 4. WP_TEMP_DIR (if defined in wp-config.php)
+ 5. System temporary directory
+ 6. Manual file creation as last resort
+
+### Fixed
+- **PCLZIP Error Resolution**: Addresses `PCLZIP_ERR_MISSING_FILE (-4)` errors caused by restrictive /tmp directory permissions
+- **File Write Verification**: Added byte-level verification to ensure complete file downloads
+- **Permission Issues**: Better handling of directory permission problems during plugin updates
+
+### Documentation
+- **Integration Examples**: Added examples for handling PCLZIP errors in integration guide
+- **WordPress Configuration**: Documented wp-config.php approach for setting custom temporary directories
+- **Troubleshooting Guide**: Comprehensive examples for different temporary directory configuration strategies
+
+## [1.1.2] - 2025-08-19
+### Changed
+- **API Accessibility**: Changed `getLatestVersion()` method visibility from `private` to `public` to allow external access from consuming plugins
+
+## [1.1.1] - 2025-08-14
+### Changed
+- **License Migration**: Updated from GPL v2.0+ to PolyForm Noncommercial 1.0.0
+- **License References**: Updated all references in composer.json, README.md, source files, and GitHub Actions workflow
+- **License Documentation**: Updated license badges and documentation to reflect noncommercial licensing
+
+## [1.1.0] - 2025-08-12
+### Added
+- **Configurable text domain support**: New `text_domain` option in `UpdaterConfig` constructor for internationalization flexibility
+- **Translation wrapper methods**: Added `__()` and `esc_html__()` methods for package-aware translations
+- **Translatable user-facing messages**: All changelog and error messages now support internationalization
+- **Centralized HTTP header management**: Added `getApiHeaders()` and `getDownloadHeaders()` methods for consistent API communication
+- **Comprehensive PHP coding standards**: Implemented phpcs.xml with WordPress and PSR-12 standards enforcement
+- **String quotation standardization**: All strings now consistently use double quotes as per project standards
+- **Enhanced type declarations**: Full PHP 8+ type hint coverage with union types and nullable parameters
+- **Enhanced PHPDoc documentation**: Comprehensive descriptions and `@since` annotations throughout codebase
+- **GitHub Actions workflow**: Automated release creation when tags are pushed
+- **Automated testing in CI**: PHPUnit and PHPCS validation in release pipeline
+- **Release documentation**: Automated generation of release notes from CHANGELOG.md
+- **Package validation**: Automated structure validation and version consistency checks
+
+### Changed
+- **Improved internationalization architecture**: Text domain now configurable per consuming plugin instead of hardcoded
+- **Centralized translation system**: All user-facing strings now use configurable text domain with fallback support
+- **Refactored HTTP request configurations**: Eliminated code duplication through centralized header management patterns
+- **Code quality enforcement**: Added automated coding standards checking with phpcs and WordPress security rules
+- **Documentation standards**: Enhanced PHPDoc blocks with complete parameter and return type documentation
+- **Updated User-Agent headers**: Now include version information (WP-GitHub-Updater/1.1.0)
+
+### Fixed
+- **Backward compatibility**: Existing code without `text_domain` specification continues working with `wp-github-updater` fallback
+- **String consistency**: Eliminated mixed quote usage throughout codebase for improved maintainability
+- **Security compliance**: Enhanced input sanitization and output escaping validation
+- **GitHub API request reliability**: Improved through consistent header usage
+- **Download stability**: Optimized headers for GitHub asset downloads
+
+### Technical Improvements
+- Updated User-Agent headers to version 1.1.0
+- Added Composer dev dependencies: `wp-coding-standards/wpcs` and `slevomat/coding-standard`
+- Implemented comprehensive test coverage for new translation features
+- Enhanced error handling with proper WordPress i18n integration
+- Improved code maintainability through centralized header management patterns
+
## [1.0.1] - 2025-08-07
### Added
- Markdown to HTML parser for changelog display
diff --git a/LICENSE.md b/LICENSE.md
index ae0400d..e469967 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,61 +1,25 @@
-GNU GENERAL PUBLIC LICENSE
-Version 2, June 1991
+Polyform Noncommercial License 1.0.0
-Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-Everyone is permitted to copy and distribute verbatim copies
-of this license document, but changing it is not allowed.
+Acceptance
+In order to receive this license, you must agree to its rules. The rules of this license are both obligations under that agreement and conditions to your license. You must not do anything with the licensed material that triggers a rule you cannot or will not follow.
-For details, see https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+Copyright License
+The licensor grants you a copyright license for the licensed material to do everything you might do with the licensed material that would otherwise infringe the licensor’s copyright in it, for any noncommercial purpose, for the duration of the license, and in all territories. This license does not grant you rights to use the licensed material for commercial purposes.
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
+Commercial Purposes
+Commercial purposes means use of the licensed material for a purpose intended for or directed toward commercial advantage or monetary compensation.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
+Distribution and Modification
+You may distribute copies of the licensed material, in whole or in part, in any medium, and you may create and distribute modified versions of the licensed material, provided that you (a) do not use the licensed material for commercial purposes, and (b) include a complete copy of this license with your distribution.
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Patent License
+The licensor grants you a patent license for the licensed material to make, have made, use, sell, offer for sale, and import the licensed material for any noncommercial purpose, for the duration of the license, and in all territories. This license does not grant you rights to use the licensed material for commercial purposes.
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+No Other Rights
+These terms do not grant you any other rights in the licensed material, and all other rights are reserved by the licensor.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+Termination
+If you use the licensed material in violation of this license, such use will automatically terminate your rights under this license. However, if you comply with this license after such violation, your rights will be reinstated unless the licensor has terminated this license by giving you written notice.
-Copyright (c) 2025 Silver Assist
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+No Liability
+As far as the law allows, the licensed material comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages related to this license or use of the licensed material, under any kind of legal claim.
\ No newline at end of file
diff --git a/README.md b/README.md
index 548e1f0..026c727 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# WordPress GitHub Updater
[](https://packagist.org/packages/silverassist/wp-github-updater)
-[](LICENSE.md)
+[](LICENSE.md)
[](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.
@@ -14,6 +14,9 @@ A reusable WordPress plugin updater that handles automatic updates from public G
- 🛡️ **Security**: AJAX nonce verification and capability checks
- 🔧 **Configurable**: Flexible configuration options
- 📦 **Easy Integration**: Simple Composer installation
+- 📢 **Admin Notices**: WordPress admin notifications for available updates
+- 🗂️ **Enhanced File Handling**: Multi-tier temporary file creation to resolve hosting issues
+- ✅ **Manual Version Checks**: AJAX-powered manual update checking with immediate admin feedback
## Installation
@@ -60,11 +63,13 @@ $config = new UpdaterConfig(
'plugin_author' => 'Your Name',
'plugin_homepage' => 'https://your-website.com',
'requires_wordpress' => '6.0',
- 'requires_php' => '8.0',
+ 'requires_php' => '8.3',
'asset_pattern' => '{slug}-v{version}.zip', // GitHub release asset filename
'cache_duration' => 12 * 3600, // 12 hours in seconds
'ajax_action' => 'my_plugin_check_version',
- 'ajax_nonce' => 'my_plugin_version_nonce'
+ 'ajax_nonce' => 'my_plugin_version_nonce',
+ 'text_domain' => 'my-plugin-textdomain', // For internationalization
+ 'custom_temp_dir' => WP_CONTENT_DIR . '/temp', // Custom temporary directory (v1.1.3+)
]
);
@@ -80,11 +85,25 @@ $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.0'` | Minimum PHP version |
+| `requires_php` | string | `'8.3'` | 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)** |
+
+### Internationalization Support (i18n)
+
+Starting with version 1.1.0, you can specify a custom text domain for your plugin's translations:
+
+```php
+$config = new UpdaterConfig(__FILE__, "username/repository", [
+ "text_domain" => "my-plugin-textdomain" // Use your plugin's text domain
+]);
+```
+
+**Backward Compatibility**: Existing code without the `text_domain` option will continue to work using the default `wp-github-updater` text domain.
## GitHub Release Requirements
@@ -140,7 +159,7 @@ Here's a complete example for a WordPress plugin:
* Version: 1.0.0
* Author: Your Name
* Requires at least: 6.0
- * Requires PHP: 8.0
+ * Requires PHP: 8.3
*/
// Prevent direct access
@@ -172,21 +191,174 @@ add_action('init', function() {
// Your plugin code here...
```
+## Troubleshooting
+
+### PCLZIP_ERR_MISSING_FILE (-4) Error
+
+If you encounter the error "The package could not be installed. PCLZIP_ERR_MISSING_FILE (-4)", this typically indicates issues with the temporary directory. The updater includes multiple fallback strategies to resolve this.
+
+#### Solution 1: Custom Temporary Directory (Recommended)
+
+```php
+$config = new UpdaterConfig(
+ __FILE__,
+ 'your-username/your-repo',
+ [
+ 'custom_temp_dir' => WP_CONTENT_DIR . '/temp',
+ // or use uploads directory:
+ // 'custom_temp_dir' => wp_upload_dir()['basedir'] . '/temp',
+ ]
+);
+```
+
+#### Solution 2: WordPress Configuration
+
+Add to your `wp-config.php` file (before "That's all, stop editing!"):
+
+```php
+/* Set WordPress temporary directory */
+define('WP_TEMP_DIR', ABSPATH . 'wp-content/temp');
+```
+
+Then create the directory with proper permissions:
+
+```bash
+mkdir wp-content/temp
+chmod 755 wp-content/temp
+```
+
+#### Solution 3: Plugin Activation Hook
+
+Create the temporary directory when your plugin is activated:
+
+```php
+register_activation_hook(__FILE__, function() {
+ $temp_dir = WP_CONTENT_DIR . '/temp';
+ if (!file_exists($temp_dir)) {
+ wp_mkdir_p($temp_dir);
+ }
+});
+```
+
+### Multi-Tier Fallback System
+
+The updater automatically tries multiple strategies for temporary file creation:
+
+1. **Custom temporary directory** (if configured)
+2. **WordPress uploads directory**
+3. **WP_CONTENT_DIR/temp** (auto-created)
+4. **WP_TEMP_DIR** (if defined in wp-config.php)
+5. **System temporary directory** (/tmp)
+6. **Manual file creation** (last resort)
+
## Requirements
-- PHP 8.0 or higher
+- PHP 8.2 or higher
- WordPress 6.0 or higher
- Composer for dependency management
- Public GitHub repository with releases
## Development
-### Running Tests
+### Testing
+
+The package includes comprehensive testing (51 tests, 130 assertions, 100% passing):
+
+**Test Coverage:**
+- **Unit Tests** (3 tests): Configuration and core functionality
+- **Integration Tests** (22 tests): Updater + Config integration, download filters, **real GitHub API** ⭐
+- **WordPress Tests** (26 tests): Hooks, filters, and mock plugin integration
+
+**Running Tests:**
```bash
+# Install development dependencies
+composer install --dev
+
+# Run all tests
composer test
+
+# Run specific test suites
+./scripts/test-runner.sh unit # Unit tests only
+./scripts/test-runner.sh integration # Integration tests (includes real GitHub API)
+./scripts/test-runner.sh wordpress # WordPress integration tests
+./scripts/test-runner.sh all # All tests
+
+# Run with coverage
+vendor/bin/phpunit --coverage-text
```
+**Real GitHub API Testing:**
+
+The integration tests include **real HTTP requests** to production GitHub repositories to verify actual API behavior:
+
+- ✅ Validates actual GitHub API response structure
+- ✅ Verifies caching performance (< 10ms for cached calls)
+- ✅ Tests version comparison with real releases
+- ✅ Confirms asset pattern matching with production URLs
+
+**Example: Test with Your Own Repository**
+
+```php
+// tests/Integration/MyRealAPITest.php
+public function testFetchLatestVersionFromMyRepo(): void {
+ $config = new UpdaterConfig([
+ "plugin_file" => __FILE__,
+ "github_username" => "YourUsername",
+ "github_repo" => "your-repository",
+ ]);
+
+ $updater = new Updater($config);
+ $version = $updater->getLatestVersion();
+
+ $this->assertNotFalse($version);
+ $this->assertMatchesRegularExpression("/^\d+\.\d+\.\d+$/", $version);
+}
+```
+
+**Test Environment Setup:**
+
+The tests use WordPress Test Suite for authentic WordPress integration:
+
+```bash
+# Install WordPress Test Suite (interactive)
+./scripts/test-runner.sh install
+
+# Or manual installation
+./scripts/install-wp-tests.sh wordpress_test root '' localhost latest
+```
+
+**PCLZIP Error Testing:**
+
+For testing plugins that may experience `PCLZIP_ERR_MISSING_FILE (-4)` errors, configure a custom temporary directory:
+
+```php
+$config = new UpdaterConfig(
+ __FILE__,
+ "your-username/your-plugin",
+ [
+ "custom_temp_dir" => WP_CONTENT_DIR . "/temp",
+ ]
+);
+```
+
+### PHPUnit Version Policy
+
+**This package uses PHPUnit 9.6.x and MUST remain on this version.**
+
+**Why PHPUnit 9.6?**
+- ✅ **WordPress Ecosystem Standard**: Most WordPress projects use PHPUnit 9.6
+- ✅ **WordPress Coding Standards Compatible**: [WordPress Coding Standards](https://github.com/WordPress/WordPress-Coding-Standards) uses PHPUnit 9.x
+- ✅ **Yoast PHPUnit Polyfills**: Version 4.x supports PHPUnit 7.5-9.x, 11.x, 12.x, but **NOT 10.x**
+- ✅ **Consumer Compatibility**: Projects depending on this package expect PHPUnit 9.6
+
+**Do NOT upgrade to:**
+- ❌ PHPUnit 10.x (incompatible with Yoast PHPUnit Polyfills 4.x)
+- ❌ PHPUnit 11.x or 12.x (breaks compatibility with most WordPress projects)
+
+**Dependabot Configuration:**
+The `.github/dependabot.yml` file is configured to automatically ignore PHPUnit major version updates, ensuring the package remains on 9.x.
+
### Code Standards
```bash
@@ -206,6 +378,38 @@ composer phpstan
composer check
```
+## Public API Methods
+
+The updater provides several public methods for programmatic access:
+
+### Version Information
+
+```php
+// Check if an update is available
+$hasUpdate = $updater->isUpdateAvailable(); // Returns bool
+
+// Get the current plugin version
+$currentVersion = $updater->getCurrentVersion(); // Returns string
+
+// Get the latest version from GitHub (with caching)
+$latestVersion = $updater->getLatestVersion(); // Returns string|false
+
+// Get the GitHub repository
+$repo = $updater->getGithubRepo(); // Returns string
+```
+
+### Manual Version Check
+
+You can trigger a manual version check programmatically:
+
+```php
+// This will clear caches and check for updates
+// If an update is available, it will set an admin notice
+$updater->manualVersionCheck();
+```
+
+**Note**: The manual version check method is designed for AJAX calls and will send JSON responses. For programmatic use, prefer the individual methods above.
+
## Contributing
1. Fork the repository
@@ -216,7 +420,7 @@ composer check
## License
-This package is licensed under the GNU General Public License v2.0 or later (GPL-2.0-or-later). Please see [License File](LICENSE.md) for more information.
+This package is licensed under the Polyform Noncommercial License 1.0.0 (PolyForm-Noncommercial-1.0.0). This license allows for non-commercial use, modification, and distribution. Please see [License File](LICENSE.md) for more information.
## Credits
@@ -229,10 +433,13 @@ Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed re
## Security
-If you discover any security related issues, please email security@silverassist.com instead of using the issue tracker.
+If you discover any security related issues, please report them via [GitHub Issues](https://github.com/SilverAssist/wp-github-updater/issues) with the "security" label.
## Support
- **Documentation**: This README and code comments
- **Issues**: [GitHub Issues](https://github.com/SilverAssist/wp-github-updater/issues)
-- **Discussions**: [GitHub Discussions](https://github.com/SilverAssist/wp-github-updater/discussions)
+
+---
+
+**Made with ❤️ by [Silver Assist](https://silverassist.com)**
\ No newline at end of file
diff --git a/composer.json b/composer.json
index df4f5a0..95cd4dd 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,7 @@
{
"name": "silverassist/wp-github-updater",
"description": "A reusable WordPress plugin updater that handles automatic updates from public GitHub releases",
+ "version": "1.2.1",
"type": "library",
"keywords": [
"wordpress",
@@ -10,7 +11,7 @@
"auto-update"
],
"homepage": "https://github.com/SilverAssist/wp-github-updater",
- "license": "GPL-2.0-or-later",
+ "license": "PolyForm-Noncommercial-1.0.0",
"authors": [
{
"name": "Silver Assist",
@@ -19,12 +20,19 @@
}
],
"require": {
- "php": ">=8.0"
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^10.0",
+ "phpunit/phpunit": "^9.6",
"squizlabs/php_codesniffer": "^3.7",
- "phpstan/phpstan": "^1.10"
+ "phpstan/phpstan": "^1.10",
+ "wp-coding-standards/wpcs": "^3.0",
+ "slevomat/coding-standard": "^8.0",
+ "php-stubs/wordpress-stubs": "^6.8",
+ "php-stubs/wordpress-tests-stubs": "^6.8",
+ "phpcompatibility/phpcompatibility-wp": "^2.1",
+ "szepeviktor/phpstan-wordpress": "^1.3",
+ "yoast/phpunit-polyfills": "^4.0"
},
"autoload": {
"psr-4": {
@@ -38,8 +46,11 @@
},
"scripts": {
"test": "phpunit",
- "phpcs": "phpcs --standard=PSR12 src/ tests/",
- "phpcbf": "phpcbf --standard=PSR12 src/ tests/",
+ "test:unit": "phpunit --testsuite unit",
+ "test:integration": "phpunit --testsuite integration",
+ "test:wordpress": "phpunit --testsuite wordpress",
+ "phpcs": "phpcs",
+ "phpcbf": "phpcbf",
"phpstan": "phpstan analyse src/ --level=8",
"check": [
"@phpcs",
@@ -49,7 +60,10 @@
},
"config": {
"sort-packages": true,
- "optimize-autoloader": true
+ "optimize-autoloader": true,
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
},
"minimum-stability": "stable",
"prefer-stable": true
diff --git a/examples/integration-guide.php b/examples/integration-guide.php
index 057b5dd..62c6b49 100644
--- a/examples/integration-guide.php
+++ b/examples/integration-guide.php
@@ -22,18 +22,27 @@ use SilverAssist\WpGithubUpdater\Updater;
use SilverAssist\WpGithubUpdater\UpdaterConfig;
// Initialize updater (replaces your existing Updater class instantiation)
-add_action('init', function() {
+add_action("init", function() {
$config = new UpdaterConfig(
__FILE__, // Path to your main plugin file
- 'SilverAssist/silver-assist-security', // Your GitHub repo
+ "SilverAssist/silver-assist-security", // Your GitHub repo
[
- 'asset_pattern' => 'silver-assist-security-v{version}.zip',
- 'ajax_action' => 'silver_assist_security_check_version',
- 'ajax_nonce' => 'silver_assist_security_ajax'
+ "asset_pattern" => "silver-assist-security-v{version}.zip",
+ "ajax_action" => "silver_assist_security_check_version",
+ "ajax_nonce" => "silver_assist_security_ajax",
+ "text_domain" => "silver-assist-security", // Your plugin's text domain
+ "custom_temp_dir" => WP_CONTENT_DIR . "/temp", // Enhanced error handling (v1.1.3+)
]
);
- new Updater($config);
+ $updater = new Updater($config);
+
+ // Optional: Programmatic version checking (v1.1.2+)
+ if ($updater->isUpdateAvailable()) {
+ // Handle update availability programmatically
+ $latestVersion = $updater->getLatestVersion();
+ error_log("Security plugin update available: " . $latestVersion);
+ }
});
*/
@@ -50,18 +59,27 @@ require_once __DIR__ . '/vendor/autoload.php';
use SilverAssist\WpGithubUpdater\Updater;
use SilverAssist\WpGithubUpdater\UpdaterConfig;
-add_action('init', function() {
+add_action("init", function() {
$config = new UpdaterConfig(
__FILE__,
- 'your-username/leadgen-app-form', // Your GitHub repo
+ "your-username/leadgen-app-form", // Your GitHub repo
[
- 'asset_pattern' => 'leadgen-app-form-v{version}.zip',
- 'ajax_action' => 'leadgen_check_version',
- 'ajax_nonce' => 'leadgen_version_check'
+ "asset_pattern" => "leadgen-app-form-v{version}.zip",
+ "ajax_action" => "leadgen_check_version",
+ "ajax_nonce" => "leadgen_version_check",
+ "text_domain" => "leadgen-app-form", // Your plugin's text domain
+ "custom_temp_dir" => wp_upload_dir()["basedir"] . "/temp", // Alternative temp dir location
]
);
- new Updater($config);
+ $updater = new Updater($config);
+
+ // Optional: Add manual check button in admin
+ add_action("admin_init", function() use ($updater) {
+ if (isset($_GET["leadgen_check_update"]) && current_user_can("update_plugins")) {
+ $updater->manualVersionCheck();
+ }
+ });
});
*/
@@ -89,24 +107,87 @@ use SilverAssist\WpGithubUpdater\Updater;
use SilverAssist\WpGithubUpdater\UpdaterConfig;
// Initialize the updater
-add_action('init', function() {
+add_action("init", function() {
$config = new UpdaterConfig(
__FILE__,
- 'your-username/my-new-plugin', // GitHub repository
+ "your-username/my-new-plugin", // GitHub repository
[
// Optional customizations
- 'asset_pattern' => 'my-plugin-{version}.zip',
- 'requires_php' => '8.1',
- 'requires_wordpress' => '6.2',
- 'ajax_action' => 'my_plugin_version_check',
- 'cache_duration' => 6 * 3600 // 6 hours
+ "asset_pattern" => "my-plugin-{version}.zip",
+ "requires_php" => "8.1",
+ "requires_wordpress" => "6.2",
+ "ajax_action" => "my_plugin_version_check",
+ "cache_duration" => 6 * 3600, // 6 hours
+ "text_domain" => "my-new-plugin", // Your plugin's text domain
+ "custom_temp_dir" => WP_CONTENT_DIR . "/temp", // Improved hosting compatibility
+ ]
+ );
+
+ $updater = new Updater($config);
+
+ // Example: Check for updates programmatically
+ add_action("admin_notices", function() use ($updater) {
+ if (!current_user_can("update_plugins")) return;
+
+ if ($updater->isUpdateAvailable()) {
+ $currentVersion = $updater->getCurrentVersion();
+ $latestVersion = $updater->getLatestVersion();
+
+ echo '
';
+ echo '
My Plugin: Update available from ' . esc_html($currentVersion) . ' to ' . esc_html($latestVersion) . '
';
+ echo '
';
+ }
+ });
+});
+
+// Your plugin code here...
+*/
+
+/**
+ * Example for plugins with PCLZIP_ERR_MISSING_FILE issues
+ * Use custom temporary directory to avoid /tmp permission problems
+ */
+
+/*
+// For plugins experiencing PCLZIP_ERR_MISSING_FILE errors:
+
+add_action("init", function() {
+ $config = new UpdaterConfig(
+ __FILE__,
+ "your-username/your-plugin",
+ [
+ "text_domain" => "your-plugin",
+ "custom_temp_dir" => WP_CONTENT_DIR . "/temp", // Custom temp directory
+ // or use uploads directory:
+ // "custom_temp_dir" => wp_upload_dir()["basedir"] . "/temp",
]
);
new Updater($config);
+
+ // Optionally create the directory on plugin activation
+ register_activation_hook(__FILE__, function() {
+ $temp_dir = WP_CONTENT_DIR . "/temp";
+ if (!file_exists($temp_dir)) {
+ wp_mkdir_p($temp_dir);
+ }
+ });
});
+*/
-// Your plugin code here...
+/**
+ * Alternative: WordPress configuration approach
+ * Add this to your wp-config.php file (before the line that says
+ * "That's all, stop editing!"):
+ */
+
+/*
+// In wp-config.php, add this line:
+define('WP_TEMP_DIR', ABSPATH . 'wp-content/temp');
+
+// Then create the directory with proper permissions:
+// mkdir wp-content/temp
+// chmod 755 wp-content/temp
*/
/**
@@ -118,3 +199,22 @@ add_action('init', function() {
* 4. Remove your old updater class files
* 5. Test the updates
*/
+
+/**
+ * New Features in v1.1.4:
+ *
+ * - WordPress admin notices for manual version checks
+ * - Dismissible admin notices with AJAX functionality
+ * - Improved code organization with isUpdateAvailable() method
+ *
+ * New Features in v1.1.3:
+ *
+ * - Enhanced temporary file handling to resolve PCLZIP errors
+ * - Better error handling and hosting environment compatibility
+ *
+ * Public API methods (v1.1.2+):
+ * - $updater->isUpdateAvailable() - Check if update is available
+ * - $updater->getCurrentVersion() - Get current plugin version
+ * - $updater->getLatestVersion() - Get latest GitHub version
+ * - $updater->getGithubRepo() - Get repository name
+ */
diff --git a/phpcs.xml b/phpcs.xml
index 3717dba..830a943 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -1,5 +1,24 @@
+
@@ -14,6 +33,74 @@
srctests
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tests/bootstrap.php
+ tests/fixtures/*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tests/*
+
+
+
+
+
+ warning
+
+
+
+
+ 0
+
+
+
+ 0
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+ vendor/*
+ *.min.*
diff --git a/phpstan.neon b/phpstan.neon
index 65ed03a..dffa685 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,9 +1,16 @@
+includes:
+ - vendor/szepeviktor/phpstan-wordpress/extension.neon
+
parameters:
level: 8
paths:
- src
excludePaths:
- tests
+ bootstrapFiles:
+ - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
+ scanDirectories:
+ - vendor/php-stubs/wordpress-stubs
ignoreErrors:
- # WordPress functions are not available during static analysis
- - '#Call to unknown function: [\\]?[a-zA-Z_]+#'
+ # Ignore WP_Error union type issues in some contexts
+ - '#Cannot call method get_error_message\(\) on string\|WP_Error\|false#'
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..fb63689
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,37 @@
+
+
+
+
+ tests/Unit
+
+
+ tests/Integration
+
+
+ tests/WordPress
+
+
+
+
+ ./src
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/install-wp-tests.sh b/scripts/install-wp-tests.sh
new file mode 100755
index 0000000..63a2b7c
--- /dev/null
+++ b/scripts/install-wp-tests.sh
@@ -0,0 +1,255 @@
+#!/usr/bin/env bash
+# WordPress Test Environment Installation Script
+# Based on WordPress Core test suite installer
+#
+# This script installs WordPress and the WordPress Test Suite for running PHPUnit tests
+# Usage: ./install-wp-tests.sh [db-host] [wp-version]
+
+if [ $# -lt 3 ]; then
+ echo ""
+ echo "❌ ERROR: Missing required arguments"
+ echo ""
+ echo "Usage: $0 [db-host] [wp-version]"
+ echo ""
+ echo "Arguments:"
+ echo " db-name Database name for tests (will be created/recreated)"
+ echo " db-user MySQL username"
+ echo " db-pass MySQL password (use '' for empty password)"
+ echo " db-host MySQL host (default: localhost)"
+ echo " wp-version WordPress version to install (default: 6.7.1)"
+ echo ""
+ echo "Example:"
+ echo " $0 wordpress_test root '' localhost 6.7.1"
+ echo ""
+ exit 1
+fi
+
+DB_NAME=$1
+DB_USER=$2
+DB_PASS=$3
+DB_HOST=${4-localhost}
+WP_VERSION=${5-6.7.1}
+SKIP_DB_CREATE=${6-false}
+
+TMPDIR=${TMPDIR-/tmp}
+TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
+WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}
+WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/}
+
+echo ""
+echo "🚀 WordPress Test Suite Installer"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo "Installing WordPress Test Environment..."
+echo ""
+echo "Configuration:"
+echo " Database Name: $DB_NAME"
+echo " Database User: $DB_USER"
+echo " Database Host: $DB_HOST"
+echo " WordPress Version: $WP_VERSION"
+echo " WP Tests Dir: $WP_TESTS_DIR"
+echo " WP Core Dir: $WP_CORE_DIR"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo ""
+
+download() {
+ if [ `which curl` ]; then
+ curl -s "$1" > "$2";
+ elif [ `which wget` ]; then
+ wget -nv -O "$2" "$1"
+ fi
+}
+
+if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
+ WP_BRANCH=${WP_VERSION%\-*}
+ WP_TESTS_TAG="branches/$WP_BRANCH"
+
+elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
+ WP_TESTS_TAG="branches/$WP_VERSION"
+
+elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
+ if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
+ WP_TESTS_TAG="tags/${WP_VERSION%??}"
+ else
+ WP_TESTS_TAG="tags/$WP_VERSION"
+ fi
+
+elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' || $WP_VERSION == 'latest' ]]; then
+ WP_TESTS_TAG="trunk"
+
+else
+ echo "Invalid version $WP_VERSION"
+ exit 1
+fi
+
+set -ex
+
+install_wp() {
+
+ if [ -d $WP_CORE_DIR ]; then
+ echo "✅ WordPress core already installed at $WP_CORE_DIR"
+ return;
+ fi
+
+ echo "📥 Downloading WordPress $WP_VERSION..."
+ mkdir -p $WP_CORE_DIR
+
+ if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
+ mkdir -p $TMPDIR/wordpress-trunk
+ rm -rf $TMPDIR/wordpress-trunk/*
+ svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress
+ mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR
+ else
+ if [ $WP_VERSION == 'latest' ]; then
+ local ARCHIVE_NAME='latest'
+ elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
+ if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
+ local ARCHIVE_NAME="${WP_VERSION%??}"
+ else
+ local ARCHIVE_NAME=$WP_VERSION
+ fi
+ else
+ local ARCHIVE_NAME="wordpress-$WP_VERSION"
+ fi
+ download https://wordpress.org/wordpress-${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz
+ tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
+ fi
+
+ echo "✅ WordPress $WP_VERSION downloaded successfully"
+
+ echo "📥 Downloading mysqli drop-in..."
+ download https://raw.githubusercontent.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
+ echo "✅ mysqli drop-in installed"
+}
+
+install_test_suite() {
+ if [ -d $WP_TESTS_DIR ]; then
+ echo "✅ WordPress Test Suite already installed at $WP_TESTS_DIR"
+ return;
+ fi
+
+ echo "📥 Downloading WordPress Test Suite from SVN..."
+ mkdir -p $WP_TESTS_DIR
+
+ rm -rf $WP_TESTS_DIR/{includes,data}
+
+ svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
+ svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
+
+ echo "✅ WordPress Test Suite downloaded successfully"
+
+ echo "📝 Generating wp-tests-config.php..."
+ if [ ! -f wp-tests-config.php ]; then
+ download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
+ WP_CORE_DIR=$(echo $WP_CORE_DIR | sed 's:/\+$::')
+ sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s:__FILE__:'$WP_TESTS_DIR/wp-tests-config.php':" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
+ fi
+ echo "✅ wp-tests-config.php generated"
+}
+
+recreate_db() {
+ shopt -s nocasematch
+ if [[ $1 =~ ^(y|yes)$ ]]
+ then
+ mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA
+ create_db
+ echo "Recreated the database ($DB_NAME)."
+ else
+ echo "Skipping database recreation."
+ exit 1
+ fi
+ shopt -u nocasematch
+}
+
+create_db() {
+ mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
+}
+
+install_db() {
+ if [ ${SKIP_DB_CREATE} = "true" ]; then
+ return 0
+ fi
+
+ # Use DB_HOST for compatibility (script historically used DB_HOSTNAME)
+ DB_HOSTNAME=${DB_HOST}
+
+ EXTRA=""
+
+ if ! [ -z "$DB_HOSTNAME" ] ; then
+ # If hostname starts with /, it's a socket path
+ if [[ $DB_HOSTNAME == /* ]] ; then
+ EXTRA=" --socket=$DB_HOSTNAME"
+ # If hostname contains a colon, it's host:port
+ elif [[ $DB_HOSTNAME == *:* ]] ; then
+ EXTRA=" --host=$(echo $DB_HOSTNAME | cut -d: -f1) --port=$(echo $DB_HOSTNAME | cut -d: -f2) --protocol=tcp"
+ # Otherwise it's just a hostname or IP - use TCP
+ else
+ EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
+ fi
+ fi
+
+ if [ -n "`mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$`" ]
+ then
+ # In CI/CD or non-interactive mode, automatically recreate database
+ if [ -t 0 ]; then
+ # Interactive mode - ask for confirmation
+ echo ""
+ echo "⚠️ DATABASE ALREADY EXISTS"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "The database '$DB_NAME' already exists in your MySQL server."
+ echo ""
+ echo "WordPress Test Suite requires a clean database installation."
+ echo "The existing database will be DROPPED and recreated."
+ echo ""
+ echo "⚠️ WARNING: This will DELETE all data in the '$DB_NAME' database!"
+ echo ""
+ echo "If this is a production database or contains important data,"
+ echo "press Ctrl+C now to cancel, or type 'N' below."
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ read -p "Are you sure you want to proceed? [y/N]: " DELETE_EXISTING_DB
+ recreate_db $DELETE_EXISTING_DB
+ else
+ # Non-interactive mode (CI/CD) - automatically recreate
+ echo "🔄 Database already exists - automatically recreating for test environment..."
+ mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA
+ create_db
+ echo "✅ Database recreated successfully"
+ fi
+ else
+ create_db
+ fi
+}
+
+case $(uname -s) in
+ Darwin)
+ ioption='-i.bak'
+ ;;
+ *)
+ ioption='-i'
+ ;;
+esac
+
+install_wp
+install_test_suite
+install_db
+
+echo ""
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo "✅ WordPress Test Suite installed successfully!"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo ""
+echo "Environment Variables:"
+echo " WP_TESTS_DIR: $WP_TESTS_DIR"
+echo " WP_CORE_DIR: $WP_CORE_DIR"
+echo ""
+echo "Add this to your phpunit.xml.dist:"
+echo " "
+echo ""
+echo "Now you can run tests with:"
+echo " vendor/bin/phpunit --testdox"
+echo ""
diff --git a/scripts/test-runner.sh b/scripts/test-runner.sh
new file mode 100755
index 0000000..a9b319d
--- /dev/null
+++ b/scripts/test-runner.sh
@@ -0,0 +1,231 @@
+#!/bin/bash
+# WordPress Test Suite Setup and Test Runner
+# This script helps install WordPress Test Suite and run tests
+
+set -e
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Script directory
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
+
+echo -e "${BLUE}================================${NC}"
+echo -e "${BLUE}WP GitHub Updater Test Runner${NC}"
+echo -e "${BLUE}================================${NC}"
+echo ""
+
+# Function to check if WordPress Test Suite is installed
+check_wp_tests() {
+ local tests_dir="${WP_TESTS_DIR:-/tmp/wordpress-tests-lib}"
+
+ if [ -d "$tests_dir" ] && [ -f "$tests_dir/includes/functions.php" ]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# Function to install WordPress Test Suite
+install_wp_tests() {
+ echo -e "${YELLOW}WordPress Test Suite not found${NC}"
+ echo ""
+ echo "To run WordPress integration tests, you need to install WordPress Test Suite."
+ echo ""
+ echo -e "${GREEN}Installation command:${NC}"
+ echo " ./scripts/install-wp-tests.sh [db-host] [wp-version]"
+ echo ""
+ echo -e "${GREEN}Example:${NC}"
+ echo " ./scripts/install-wp-tests.sh wordpress_test root '' localhost 6.7.1"
+ echo ""
+ read -p "Do you want to install now? (y/N) " -n 1 -r
+ echo
+
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ echo ""
+ echo "Please provide database credentials:"
+ read -p "Database name (default: wordpress_test): " db_name
+ db_name=${db_name:-wordpress_test}
+
+ read -p "Database user (default: root): " db_user
+ db_user=${db_user:-root}
+
+ read -sp "Database password (default: empty): " db_pass
+ echo
+
+ read -p "Database host (default: localhost): " db_host
+ db_host=${db_host:-localhost}
+
+ read -p "WordPress version (default: 6.7.1): " wp_version
+ wp_version=${wp_version:-6.7.1}
+
+ echo ""
+ echo -e "${BLUE}Installing WordPress Test Suite...${NC}"
+ "$PROJECT_ROOT/scripts/install-wp-tests.sh" "$db_name" "$db_user" "$db_pass" "$db_host" "$wp_version"
+
+ if [ $? -eq 0 ]; then
+ echo ""
+ echo -e "${GREEN}✓ WordPress Test Suite installed successfully!${NC}"
+ return 0
+ else
+ echo ""
+ echo -e "${RED}✗ Failed to install WordPress Test Suite${NC}"
+ return 1
+ fi
+ else
+ echo ""
+ echo "Skipping WordPress Test Suite installation."
+ echo "You can install it later using the command above."
+ return 1
+ fi
+}
+
+# Function to run specific test suite
+run_tests() {
+ local suite=$1
+ local phpunit="$PROJECT_ROOT/vendor/bin/phpunit"
+
+ if [ ! -f "$phpunit" ]; then
+ echo -e "${RED}✗ PHPUnit not found. Please run: composer install${NC}"
+ exit 1
+ fi
+
+ cd "$PROJECT_ROOT"
+
+ case $suite in
+ unit)
+ echo -e "${BLUE}Running Unit Tests...${NC}"
+ "$phpunit" --testsuite=unit
+ ;;
+ integration)
+ echo -e "${BLUE}Running Integration Tests...${NC}"
+ "$phpunit" --testsuite=integration
+ ;;
+ wordpress)
+ echo -e "${BLUE}Running WordPress Tests...${NC}"
+ if check_wp_tests; then
+ echo -e "${GREEN}✓ WordPress Test Suite found${NC}"
+ else
+ echo -e "${YELLOW}⚠️ WordPress Test Suite not found${NC}"
+ echo "WordPress tests will run with mocked functions."
+ echo ""
+ fi
+ "$phpunit" --testsuite=wordpress
+ ;;
+ all)
+ echo -e "${BLUE}Running All Tests...${NC}"
+ "$phpunit"
+ ;;
+ coverage)
+ echo -e "${BLUE}Running Tests with Coverage...${NC}"
+ if command -v php -m | grep -q xdebug; then
+ "$phpunit" --coverage-html build/coverage --coverage-text
+ echo ""
+ echo -e "${GREEN}✓ Coverage report generated in: build/coverage/index.html${NC}"
+ else
+ echo -e "${YELLOW}⚠️ Xdebug not found. Installing PCOV...${NC}"
+ echo "Please install Xdebug or PCOV for code coverage:"
+ echo " - Xdebug: pecl install xdebug"
+ echo " - PCOV: pecl install pcov"
+ exit 1
+ fi
+ ;;
+ *)
+ echo -e "${RED}Unknown test suite: $suite${NC}"
+ show_usage
+ exit 1
+ ;;
+ esac
+}
+
+# Function to show test status
+show_status() {
+ echo -e "${BLUE}Test Environment Status:${NC}"
+ echo ""
+
+ # PHPUnit
+ if [ -f "$PROJECT_ROOT/vendor/bin/phpunit" ]; then
+ echo -e "PHPUnit: ${GREEN}✓ Installed${NC}"
+ phpunit_version=$("$PROJECT_ROOT/vendor/bin/phpunit" --version | head -n 1)
+ echo " $phpunit_version"
+ else
+ echo -e "PHPUnit: ${RED}✗ Not installed${NC}"
+ echo " Run: composer install"
+ fi
+
+ echo ""
+
+ # WordPress Test Suite
+ if check_wp_tests; then
+ echo -e "WP Tests: ${GREEN}✓ Installed${NC}"
+ tests_dir="${WP_TESTS_DIR:-/tmp/wordpress-tests-lib}"
+ echo " Location: $tests_dir"
+ else
+ echo -e "WP Tests: ${YELLOW}⚠️ Not installed${NC}"
+ echo " Run: $0 install"
+ fi
+
+ echo ""
+
+ # Mock Plugin
+ mock_plugin="$PROJECT_ROOT/tests/fixtures/mock-plugin/mock-plugin.php"
+ if [ -f "$mock_plugin" ]; then
+ echo -e "Mock Plugin: ${GREEN}✓ Available${NC}"
+ echo " Location: tests/fixtures/mock-plugin/"
+ else
+ echo -e "Mock Plugin: ${RED}✗ Not found${NC}"
+ fi
+
+ echo ""
+}
+
+# Function to show usage
+show_usage() {
+ echo "Usage: $0 [command]"
+ echo ""
+ echo "Commands:"
+ echo " status Show test environment status"
+ echo " install Install WordPress Test Suite"
+ echo " unit Run unit tests only"
+ echo " integration Run integration tests only"
+ echo " wordpress Run WordPress tests only"
+ echo " all Run all tests (default)"
+ echo " coverage Run tests with code coverage"
+ echo " help Show this help message"
+ echo ""
+ echo "Examples:"
+ echo " $0 status # Check environment"
+ echo " $0 install # Install WP Test Suite"
+ echo " $0 unit # Run unit tests"
+ echo " $0 # Run all tests"
+ echo ""
+}
+
+# Main script logic
+case ${1:-all} in
+ status)
+ show_status
+ ;;
+ install)
+ install_wp_tests
+ ;;
+ unit|integration|wordpress|all|coverage)
+ run_tests "$1"
+ ;;
+ help|-h|--help)
+ show_usage
+ ;;
+ *)
+ echo -e "${RED}Unknown command: $1${NC}"
+ echo ""
+ show_usage
+ exit 1
+ ;;
+esac
+
+exit 0
diff --git a/scripts/update-version-simple.sh b/scripts/update-version-simple.sh
new file mode 100755
index 0000000..979da4a
--- /dev/null
+++ b/scripts/update-version-simple.sh
@@ -0,0 +1,298 @@
+#!/bin/bash
+
+# WP GitHub Updater - Version Update Script
+# Updates version numbers across all project files consistently
+
+set -e # Exit on any error
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Configuration
+PROJECT_NAME="WP GitHub Updater"
+COMPOSER_FILE="composer.json"
+README_FILE="README.md"
+CHANGELOG_FILE="CHANGELOG.md"
+PHP_SOURCE_DIR="src"
+
+# Function to display usage
+usage() {
+ echo -e "${BLUE}Usage: $0 [--no-confirm]${NC}"
+ echo ""
+ echo "Examples:"
+ echo " $0 1.1.1 # Update to version 1.1.1 with confirmation"
+ echo " $0 1.2.0 --no-confirm # Update to version 1.2.0 without confirmation (for CI)"
+ echo ""
+ echo "This script will update version numbers in:"
+ echo " - composer.json"
+ echo " - README.md (if version references exist)"
+ echo " - CHANGELOG.md (if unreleased section exists)"
+ exit 1
+}
+
+# Function to validate version format
+validate_version() {
+ local version=$1
+ if [[ ! $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo -e "${RED}Error: Version must be in format X.Y.Z (e.g., 1.2.3)${NC}"
+ exit 1
+ fi
+}
+
+# Function to get current version from composer.json
+get_current_version() {
+ if [[ -f "$COMPOSER_FILE" ]]; then
+ grep '"version"' "$COMPOSER_FILE" | head -n1 | sed 's/.*"version": *"\([^"]*\)".*/\1/'
+ else
+ echo "unknown"
+ fi
+}
+
+# Function to update composer.json
+update_composer_version() {
+ local new_version=$1
+
+ if [[ -f "$COMPOSER_FILE" ]]; then
+ echo -e "${YELLOW}Updating $COMPOSER_FILE...${NC}"
+
+ # Create backup
+ cp "$COMPOSER_FILE" "${COMPOSER_FILE}.backup"
+
+ # Update version using sed
+ sed -i.tmp "s/\"version\": *\"[^\"]*\"/\"version\": \"$new_version\"/" "$COMPOSER_FILE"
+ rm "${COMPOSER_FILE}.tmp"
+
+ echo -e "${GREEN}✅ Updated composer.json version to $new_version${NC}"
+ else
+ echo -e "${RED}❌ composer.json not found${NC}"
+ exit 1
+ fi
+}
+
+# Function to update CHANGELOG.md if it has unreleased section
+update_changelog_if_unreleased() {
+ local new_version=$1
+ local current_date=$(date +"%Y-%m-%d")
+
+ if [[ -f "$CHANGELOG_FILE" ]]; then
+ # Check if there's an [Unreleased] section
+ if grep -q "## \[Unreleased\]" "$CHANGELOG_FILE"; then
+ echo -e "${YELLOW}Updating CHANGELOG.md [Unreleased] section...${NC}"
+
+ # Create backup
+ cp "$CHANGELOG_FILE" "${CHANGELOG_FILE}.backup"
+
+ # Replace [Unreleased] with the new version and date
+ sed -i.tmp "s/## \[Unreleased\]/## [$new_version] - $current_date/" "$CHANGELOG_FILE"
+ rm "${CHANGELOG_FILE}.tmp"
+
+ echo -e "${GREEN}✅ Updated CHANGELOG.md [Unreleased] to [$new_version] - $current_date${NC}"
+ else
+ echo -e "${BLUE}ℹ️ No [Unreleased] section found in CHANGELOG.md, skipping${NC}"
+ fi
+ else
+ echo -e "${BLUE}ℹ️ CHANGELOG.md not found, skipping${NC}"
+ fi
+}
+
+# Function to update PHP source files
+update_php_files() {
+ local new_version=$1
+
+ if [[ -d "$PHP_SOURCE_DIR" ]]; then
+ echo -e "${YELLOW}Updating PHP source files in $PHP_SOURCE_DIR/...${NC}"
+
+ # Find all PHP files in the source directory
+ local php_files
+ php_files=$(find "$PHP_SOURCE_DIR" -name "*.php" -type f)
+
+ if [[ -n "$php_files" ]]; then
+ while IFS= read -r file; do
+ if [[ -f "$file" ]]; then
+ # Create backup
+ cp "$file" "${file}.backup"
+
+ # Update @version tags in PHP files
+ if grep -q "@version" "$file"; then
+ sed -i.tmp "s/@version [0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*/@version $new_version/" "$file"
+ rm "${file}.tmp"
+ echo -e "${GREEN} ✅ Updated @version in $(basename "$file")${NC}"
+ fi
+
+ # Update @since tags for the current version (if they exist)
+ if grep -q "@since $new_version" "$file"; then
+ echo -e "${BLUE} ℹ️ @since $new_version already present in $(basename "$file")${NC}"
+ fi
+
+ # Update version constants (if they exist)
+ if grep -q "VERSION.*=" "$file"; then
+ sed -i.tmp "s/VERSION.*=.*['\"][0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*['\"];/VERSION = \"$new_version\";/" "$file"
+ rm "${file}.tmp"
+ echo -e "${GREEN} ✅ Updated VERSION constant in $(basename "$file")${NC}"
+ fi
+ fi
+ done <<< "$php_files"
+ else
+ echo -e "${BLUE} ℹ️ No PHP files found in $PHP_SOURCE_DIR${NC}"
+ fi
+ else
+ echo -e "${BLUE}ℹ️ Directory $PHP_SOURCE_DIR not found, skipping PHP files update${NC}"
+ fi
+}
+
+# Function to show what will be updated
+show_changes_preview() {
+ local current_version=$1
+ local new_version=$2
+
+ echo -e "${BLUE}========================================${NC}"
+ echo -e "${BLUE}$PROJECT_NAME - Version Update Preview${NC}"
+ echo -e "${BLUE}========================================${NC}"
+ echo ""
+ echo -e "Current version: ${YELLOW}$current_version${NC}"
+ echo -e "New version: ${GREEN}$new_version${NC}"
+ echo ""
+ echo "Files that will be updated:"
+
+ if [[ -f "$COMPOSER_FILE" ]]; then
+ echo -e " ${GREEN}✓${NC} $COMPOSER_FILE"
+ fi
+
+ if [[ -f "$CHANGELOG_FILE" ]] && grep -q "## \[Unreleased\]" "$CHANGELOG_FILE"; then
+ echo -e " ${GREEN}✓${NC} $CHANGELOG_FILE (convert [Unreleased] to [$new_version])"
+ else
+ echo -e " ${YELLOW}⚠${NC} $CHANGELOG_FILE (no [Unreleased] section found)"
+ fi
+
+ # Show PHP files that will be updated
+ if [[ -d "$PHP_SOURCE_DIR" ]]; then
+ local php_files
+ php_files=$(find "$PHP_SOURCE_DIR" -name "*.php" -type f)
+ if [[ -n "$php_files" ]]; then
+ echo -e " ${GREEN}✓${NC} PHP files in $PHP_SOURCE_DIR/:"
+ while IFS= read -r file; do
+ echo -e " - $(basename "$file")"
+ done <<< "$php_files"
+ fi
+ fi
+
+ echo ""
+}
+
+# Function to confirm changes
+confirm_changes() {
+ local skip_confirm=$1
+
+ if [[ "$skip_confirm" != "true" ]]; then
+ echo -e "${YELLOW}Do you want to proceed with these changes? (y/N): ${NC}"
+ read -r response
+ if [[ ! "$response" =~ ^[Yy]$ ]]; then
+ echo -e "${RED}Update cancelled.${NC}"
+ exit 0
+ fi
+ fi
+}
+
+# Function to restore backups on error
+cleanup_on_error() {
+ echo -e "${RED}An error occurred. Restoring backups...${NC}"
+
+ if [[ -f "${COMPOSER_FILE}.backup" ]]; then
+ mv "${COMPOSER_FILE}.backup" "$COMPOSER_FILE"
+ echo -e "${GREEN}Restored composer.json${NC}"
+ fi
+
+ if [[ -f "${CHANGELOG_FILE}.backup" ]]; then
+ mv "${CHANGELOG_FILE}.backup" "$CHANGELOG_FILE"
+ echo -e "${GREEN}Restored CHANGELOG.md${NC}"
+ fi
+
+ # Restore PHP file backups
+ if [[ -d "$PHP_SOURCE_DIR" ]]; then
+ local php_backups
+ php_backups=$(find "$PHP_SOURCE_DIR" -name "*.php.backup" -type f 2>/dev/null || true)
+ if [[ -n "$php_backups" ]]; then
+ while IFS= read -r backup_file; do
+ if [[ -f "$backup_file" ]]; then
+ local original_file="${backup_file%.backup}"
+ mv "$backup_file" "$original_file"
+ echo -e "${GREEN}Restored $(basename "$original_file")${NC}"
+ fi
+ done <<< "$php_backups"
+ fi
+ fi
+
+ exit 1
+}
+
+# Function to clean up successful backups
+cleanup_backups() {
+ rm -f "${COMPOSER_FILE}.backup"
+ rm -f "${CHANGELOG_FILE}.backup"
+
+ # Clean up PHP file backups
+ if [[ -d "$PHP_SOURCE_DIR" ]]; then
+ find "$PHP_SOURCE_DIR" -name "*.php.backup" -type f -delete 2>/dev/null || true
+ fi
+}
+
+# Main script
+main() {
+ local new_version=$1
+ local no_confirm=$2
+
+ # Check if version is provided
+ if [[ -z "$new_version" ]]; then
+ usage
+ fi
+
+ # Validate version format
+ validate_version "$new_version"
+
+ # Get current version
+ local current_version
+ current_version=$(get_current_version)
+
+ # Set up error handler
+ trap cleanup_on_error ERR
+
+ # Show preview
+ show_changes_preview "$current_version" "$new_version"
+
+ # Confirm changes
+ confirm_changes "$no_confirm"
+
+ # Update files
+ echo -e "${BLUE}Updating version to $new_version...${NC}"
+ echo ""
+
+ update_composer_version "$new_version"
+ update_changelog_if_unreleased "$new_version"
+ update_php_files "$new_version"
+
+ # Clean up backups on success
+ cleanup_backups
+
+ echo ""
+ echo -e "${GREEN}========================================${NC}"
+ echo -e "${GREEN}✅ Version update completed successfully!${NC}"
+ echo -e "${GREEN}========================================${NC}"
+ echo ""
+ echo -e "${BLUE}Next steps:${NC}"
+ echo "1. Review the changes: git diff"
+ echo "2. Commit the changes: git add . && git commit -m \"Bump version to $new_version\""
+ echo "3. Create a tag: git tag -a v$new_version -m \"Release v$new_version\""
+ echo "4. Push changes: git push origin main --tags"
+ echo ""
+}
+
+# Parse arguments
+if [[ "$2" == "--no-confirm" ]]; then
+ main "$1" "true"
+else
+ main "$1" "false"
+fi
diff --git a/src/Updater.php b/src/Updater.php
index 1297516..66425c5 100644
--- a/src/Updater.php
+++ b/src/Updater.php
@@ -2,16 +2,20 @@
/**
* WordPress GitHub Updater
- *
+ *
* A reusable WordPress plugin updater that handles automatic updates from public GitHub releases.
*
* @package SilverAssist\WpGithubUpdater
* @author Silver Assist
- * @license GPL-2.0-or-later
+ * @version 1.2.1
+ * @license PolyForm-Noncommercial-1.0.0
*/
namespace SilverAssist\WpGithubUpdater;
+use WP_Error;
+use WP_Upgrader;
+
/**
* Main updater class that handles plugin updates from GitHub releases
*
@@ -90,7 +94,7 @@ class Updater
// Get plugin data
$this->pluginData = $this->getPluginData();
- $this->currentVersion = $this->pluginData['Version'] ?? '1.0.0';
+ $this->currentVersion = $this->pluginData["Version"] ?? "1.0.0";
$this->initHooks();
}
@@ -100,25 +104,38 @@ class Updater
*
* Sets up filters and actions needed for WordPress update system integration.
*
- * @return void
*
* @since 1.0.0
*/
private function initHooks(): void
{
- \add_filter('pre_set_site_transient_update_plugins', [$this, 'checkForUpdate']);
- \add_filter('plugins_api', [$this, 'pluginInfo'], 20, 3);
- \add_action('upgrader_process_complete', [$this, 'clearVersionCache'], 10, 2);
+ \add_filter("pre_set_site_transient_update_plugins", [$this, "checkForUpdate"]);
+ \add_filter("plugins_api", [$this, "pluginInfo"], 20, 3);
+ \add_action("upgrader_process_complete", [$this, "clearVersionCache"], 10, 2);
+
+ // Improve download reliability
+ \add_filter("upgrader_pre_download", [$this, "maybeFixDownload"], 10, 4);
// Add AJAX action for manual version check
- \add_action("wp_ajax_{$this->config->ajaxAction}", [$this, 'manualVersionCheck']);
+ \add_action("wp_ajax_{$this->config->ajaxAction}", [$this, "manualVersionCheck"]);
+
+ // Add AJAX action for dismissing update notices
+ \add_action("wp_ajax_{$this->config->ajaxAction}_dismiss_notice", [$this, "dismissUpdateNotice"]);
+
+ // Add admin notice for manual version checks
+ \add_action("admin_notices", [$this, "showUpdateNotice"]);
}
/**
* Check for plugin updates
*
- * @param mixed $transient The update_plugins transient
- * @return mixed
+ * Compares the current plugin version with the latest GitHub release
+ * and adds update information to the WordPress update transient if needed.
+ *
+ * @param mixed $transient The update_plugins transient containing current plugin versions
+ * @return mixed The modified transient with update information added if available
+ *
+ * @since 1.0.0
*/
public function checkForUpdate($transient)
{
@@ -128,16 +145,16 @@ class Updater
$latestVersion = $this->getLatestVersion();
- if ($latestVersion && version_compare($this->currentVersion, $latestVersion, '<')) {
+ if ($this->isUpdateAvailable()) {
$transient->response[$this->pluginSlug] = (object) [
- 'slug' => $this->pluginBasename,
- 'plugin' => $this->pluginSlug,
- 'new_version' => $latestVersion,
- 'url' => $this->config->pluginHomepage,
- 'package' => $this->getDownloadUrl($latestVersion),
- 'tested' => \get_bloginfo('version'),
- 'requires_php' => $this->config->requiresPHP,
- 'compatibility' => new \stdClass(),
+ "slug" => $this->pluginBasename,
+ "plugin" => $this->pluginSlug,
+ "new_version" => $latestVersion,
+ "url" => $this->config->pluginHomepage,
+ "package" => $this->getDownloadUrl($latestVersion),
+ "tested" => \get_bloginfo("version"),
+ "requires_php" => $this->config->requiresPHP,
+ "compatibility" => new \stdClass(),
];
}
@@ -147,14 +164,19 @@ class Updater
/**
* Get plugin information for the update API
*
+ * Provides detailed plugin information when WordPress requests it,
+ * including version, changelog, and download information.
+ *
* @param false|object|array $result The result object or array
- * @param string $action The type of information being requested
- * @param object $args Plugin API arguments
- * @return false|object|array
+ * @param string $action The type of information being requested
+ * @param object $args Plugin API arguments
+ * @return false|object|array Plugin information object or original result
+ *
+ * @since 1.0.0
*/
- public function pluginInfo($result, string $action, object $args)
+ public function pluginInfo(false|object|array $result, string $action, object $args): false|object|array
{
- if ($action !== 'plugin_information' || $args->slug !== $this->pluginBasename) {
+ if ($action !== "plugin_information" || $args->slug !== $this->pluginBasename) {
return $result;
}
@@ -162,31 +184,35 @@ class Updater
$changelog = $this->getChangelog();
return (object) [
- 'slug' => $this->pluginBasename,
- 'plugin' => $this->pluginSlug,
- 'version' => $latestVersion ?: $this->currentVersion,
- 'author' => $this->config->pluginAuthor,
- 'author_profile' => $this->config->pluginHomepage,
- 'requires' => $this->config->requiresWordPress,
- 'tested' => \get_bloginfo('version'),
- 'requires_php' => $this->config->requiresPHP,
- 'name' => $this->config->pluginName,
- 'homepage' => $this->config->pluginHomepage,
- 'sections' => [
- 'description' => $this->config->pluginDescription,
- 'changelog' => $changelog,
+ "slug" => $this->pluginBasename,
+ "plugin" => $this->pluginSlug,
+ "version" => $latestVersion ?: $this->currentVersion,
+ "author" => $this->config->pluginAuthor,
+ "author_profile" => $this->config->pluginHomepage,
+ "requires" => $this->config->requiresWordPress,
+ "tested" => \get_bloginfo("version"),
+ "requires_php" => $this->config->requiresPHP,
+ "name" => $this->config->pluginName,
+ "homepage" => $this->config->pluginHomepage,
+ "sections" => [
+ "description" => $this->config->pluginDescription,
+ "changelog" => $changelog,
],
- 'download_link' => $this->getDownloadUrl($latestVersion),
- 'last_updated' => $this->getLastUpdated(),
+ "download_link" => $this->getDownloadUrl($latestVersion),
+ "last_updated" => $this->getLastUpdated(),
];
}
/**
- * Get the latest version from GitHub releases
+ * Get latest version from GitHub
*
- * @return string|false
+ * Fetches the latest release version from GitHub API with caching support.
+ *
+ * @return string|false Latest version string or false if failed
+ *
+ * @since 1.0.0
*/
- public function getLatestVersion()
+ public function getLatestVersion(): string|false
{
// Check cache first
$cachedVersion = \get_transient($this->versionTransient);
@@ -196,11 +222,8 @@ class Updater
$apiUrl = "https://api.github.com/repos/{$this->config->githubRepo}/releases/latest";
$response = \wp_remote_get($apiUrl, [
- 'timeout' => 15,
- 'headers' => [
- 'Accept' => 'application/vnd.github.v3+json',
- 'User-Agent' => 'WordPress/' . \get_bloginfo('version'),
- ],
+ "timeout" => 15,
+ "headers" => $this->getApiHeaders(),
]);
if (\is_wp_error($response) || 200 !== \wp_remote_retrieve_response_code($response)) {
@@ -211,11 +234,11 @@ class Updater
$body = \wp_remote_retrieve_body($response);
$data = json_decode($body, true);
- if (!isset($data['tag_name'])) {
+ if (!isset($data["tag_name"])) {
return false;
}
- $version = ltrim($data['tag_name'], 'v');
+ $version = ltrim($data["tag_name"], "v");
// Cache the version
\set_transient($this->versionTransient, $version, $this->config->cacheDuration);
@@ -227,13 +250,21 @@ class Updater
* Get download URL for a specific version
*
* @param string $version The version to download
- * @return string
+ *
+ * @since 1.0.0
*/
private function getDownloadUrl(string $version): string
{
+ // First try to get the actual download URL from the release assets
+ $downloadUrl = $this->getAssetDownloadUrl($version);
+ if ($downloadUrl) {
+ return $downloadUrl;
+ }
+
+ // Fallback to constructed URL
$pattern = $this->config->assetPattern;
$filename = str_replace(
- ['{slug}', '{version}'],
+ ["{slug}", "{version}"],
[$this->pluginBasename, $version],
$pattern
);
@@ -241,86 +272,134 @@ class Updater
return "https://github.com/{$this->config->githubRepo}/releases/download/v{$version}/{$filename}";
}
+ /**
+ * Get actual asset download URL from GitHub API
+ *
+ * @param string $version The version to get asset URL for
+ * @return string|null Asset download URL or null if not found
+ *
+ * @since 1.1.0
+ */
+ private function getAssetDownloadUrl(string $version): ?string
+ {
+ $apiUrl = "https://api.github.com/repos/{$this->config->githubRepo}/releases/tags/v{$version}";
+
+ $response = \wp_remote_get($apiUrl, [
+ "timeout" => 10,
+ "headers" => $this->getApiHeaders(),
+ ]);
+
+ if (\is_wp_error($response) || 200 !== \wp_remote_retrieve_response_code($response)) {
+ return null;
+ }
+
+ $body = \wp_remote_retrieve_body($response);
+ $data = json_decode($body, true);
+
+ if (!isset($data["assets"]) || empty($data["assets"])) {
+ return null;
+ }
+
+ // Look for the ZIP asset
+ foreach ($data["assets"] as $asset) {
+ if (str_ends_with($asset["name"], ".zip")) {
+ return $asset["browser_download_url"];
+ }
+ }
+
+ return null;
+ }
+
/**
* Get changelog from GitHub releases
*
- * @return string
+ * Fetches release notes from GitHub API and formats them as HTML.
+ *
+ * @return string Formatted changelog HTML
+ *
+ * @since 1.0.0
*/
private function getChangelog(): string
{
$apiUrl = "https://api.github.com/repos/{$this->config->githubRepo}/releases";
$response = \wp_remote_get($apiUrl, [
- 'timeout' => 15,
- 'headers' => [
- 'Accept' => 'application/vnd.github.v3+json',
- 'User-Agent' => 'WordPress/' . \get_bloginfo('version'),
- ],
+ "timeout" => 15,
+ "headers" => $this->getApiHeaders(),
]);
if (\is_wp_error($response) || 200 !== \wp_remote_retrieve_response_code($response)) {
- return "Unable to fetch changelog. Visit the config->githubRepo}/releases\">GitHub releases page for updates.";
+ $github_link = "config->githubRepo}/releases\">"
+ . $this->config->__("GitHub releases page") . "";
+ return sprintf(
+ $this->config->__("Unable to fetch changelog. Visit the %s for updates."),
+ $github_link
+ );
}
$body = \wp_remote_retrieve_body($response);
$releases = json_decode($body, true);
if (!is_array($releases)) {
- return 'Unable to parse changelog.';
+ return $this->config->__("Unable to parse changelog.");
}
- $changelog = '';
+ $changelog = "";
foreach (array_slice($releases, 0, 5) as $release) { // Show last 5 releases
- $version = ltrim($release['tag_name'], 'v');
- $date = date('Y-m-d', strtotime($release['published_at']));
- $body = $release['body'] ?: 'No release notes provided.';
+ $version = ltrim($release["tag_name"], "v");
+ $date = date("Y-m-d", strtotime($release["published_at"]));
+ $body = $release["body"] ?: $this->config->__("No release notes provided.");
- $changelog .= "