Workflow fixes #217
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # This workflow performs comprehensive WordPress plugin compatibility and quality checks. | |
| # It runs multiple validation processes including: | |
| # - WordPress Plugin Check for WordPress.org compatibility | |
| # - PHPUnit tests across supported PHP versions (8.2, 8.3, 8.4, 8.5) | |
| # - PHP compatibility testing across multiple PHP versions (8.2, 8.3, 8.4, 8.5) | |
| # - WordPress compatibility testing across multiple WP versions (6.8, latest, nightly) | |
| # - PHPStan static analysis for WordPress-specific code quality | |
| # - WordPress security vulnerability scanning using pattern analysis | |
| # - PHPCS code standards validation for WordPress coding standards | |
| # - Code quality analysis and automated issue creation for failures | |
| # Acceptance requires passing results and artifacts for the exact tested source. | |
| name: WordPress Compatibility & Plugin Check | |
| on: | |
| # Run on pushes to main branch and on all pull requests | |
| push: | |
| branches: [ main ] | |
| pull_request: | |
| # Allow manually triggering the workflow | |
| workflow_dispatch: | |
| # Cancels all previous workflow runs for the same branch that have not yet completed | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| issues: write | |
| jobs: | |
| plugin-check: | |
| name: WP Plugin Check (PHP 8.3) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| strategy: | |
| fail-fast: false | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@v7 | |
| with: | |
| # Always fetch the latest commit, disable any caching | |
| fetch-depth: 0 | |
| clean: true | |
| - name: Setup PHP 8.3 | |
| id: setup_php_8_3 | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.3' | |
| extensions: mysqli, curl, zip, intl, gd, mbstring, fileinfo, xml | |
| coverage: none | |
| tools: composer:v2 | |
| - name: Clear any existing composer cache | |
| id: clear_any_existing_composer_cache | |
| run: | | |
| composer clear-cache || true | |
| rm -rf vendor/ composer.lock || true | |
| - name: Install Composer dependencies (no cache) | |
| id: install_composer_dependencies_no_cache | |
| run: | | |
| composer install --prefer-dist --no-progress --no-cache | |
| - name: Clean up potential artifact directories | |
| id: clean_up_potential_artifact_directories | |
| run: | | |
| echo "🧹 Removing any artifact directories to prevent conflicts..." | |
| rm -rf .reports 2>/dev/null || true | |
| rm -rf build 2>/dev/null || true | |
| echo "✅ Cleanup complete" | |
| - name: Prepare Plugin Check build directory | |
| id: prepare_plugin_check_build_directory | |
| run: | | |
| # Stdlib-only automation fixtures run on this runner, not the workstation. | |
| python3 .github/scripts/test-automation.py | |
| rm -rf plugin-check-build 2>/dev/null || true | |
| PLUGIN_SLUG="enginescript-site-optimizer" | |
| BUILD_DIR="plugin-check-build/${PLUGIN_SLUG}" | |
| mkdir -p "$BUILD_DIR" | |
| cp enginescript-site-optimizer.php "$BUILD_DIR/" | |
| cp uninstall.php "$BUILD_DIR/" | |
| cp readme.txt "$BUILD_DIR/" | |
| cp README.md "$BUILD_DIR/" | |
| cp CHANGELOG.md "$BUILD_DIR/" | |
| cp LICENSE "$BUILD_DIR/" | |
| cp -r includes "$BUILD_DIR/" | |
| cp -r languages "$BUILD_DIR/" | |
| # Inspect a release-shaped ZIP without publishing or changing the PCP input. | |
| PACKAGE_ZIP="$RUNNER_TEMP/${PLUGIN_SLUG}-candidate.zip" | |
| rm -f "$PACKAGE_ZIP" | |
| (cd plugin-check-build && zip -qr "$PACKAGE_ZIP" "$PLUGIN_SLUG") | |
| mkdir -p "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| python3 .github/scripts/check-plugin-package.py "$BUILD_DIR" --zip "$PACKAGE_ZIP" | tee "$RUNNER_TEMP/es-optimizer-diagnostics/package-manifest.txt" | |
| if [ "$(stat --format=%s "$PACKAGE_ZIP")" -gt 2097152 ]; then | |
| rm -f "$PACKAGE_ZIP" | |
| echo '::error::Candidate package exceeds the diagnostic artifact limit.' | |
| exit 1 | |
| fi | |
| - name: WordPress Plugin Check | |
| id: wordpress_plugin_check | |
| uses: WordPress/plugin-check-action@v1.1.9 | |
| with: | |
| # Comment only on trusted, same-repository human PRs. An empty token | |
| # skips the action's optional comment, not checks or annotations. | |
| repo-token: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.user.login != 'dependabot[bot]' && github.token || '' }} | |
| # Production-style plugin directory. Excludes repo-only files such as | |
| # .github, GEMINI.md, and other development artifacts. | |
| build-dir: './plugin-check-build/enginescript-site-optimizer' | |
| # Configure which categories to check | |
| categories: | | |
| accessibility | |
| general | |
| performance | |
| plugin_repo | |
| security | |
| # Whether to include experimental checks | |
| include-experimental: false | |
| # Don't ignore warnings or errors | |
| ignore-warnings: false | |
| ignore-errors: false | |
| # WordPress version to use | |
| wp-version: 'latest' | |
| - name: Handle plugin check completion | |
| id: handle_plugin_check_completion | |
| if: ${{ always() }} | |
| run: | | |
| echo "Plugin Check job status: ${{ job.status }}" | |
| - name: Preserve diagnostic summary | |
| id: preserve_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| env: | |
| STEP_RESULTS: | | |
| checkout_code=${{ steps.checkout_code.outcome }} | |
| setup_php_8_3=${{ steps.setup_php_8_3.outcome }} | |
| clear_any_existing_composer_cache=${{ steps.clear_any_existing_composer_cache.outcome }} | |
| install_composer_dependencies_no_cache=${{ steps.install_composer_dependencies_no_cache.outcome }} | |
| clean_up_potential_artifact_directories=${{ steps.clean_up_potential_artifact_directories.outcome }} | |
| prepare_plugin_check_build_directory=${{ steps.prepare_plugin_check_build_directory.outcome }} | |
| wordpress_plugin_check=${{ steps.wordpress_plugin_check.outcome }} | |
| handle_plugin_check_completion=${{ steps.handle_plugin_check_completion.outcome }} | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| printf '%s\n' "$STEP_RESULTS" > "$RUNNER_TEMP/es-optimizer-diagnostics/steps.txt" | |
| printf 'commit=%s\njob=%s\n' "$GITHUB_SHA" "$GITHUB_JOB" > "$RUNNER_TEMP/es-optimizer-diagnostics/run.txt" | |
| if command -v php >/dev/null; then | |
| php -r 'echo PHP_VERSION, PHP_EOL;' > "$RUNNER_TEMP/es-optimizer-diagnostics/php.txt" | |
| fi | |
| if command -v composer >/dev/null && [ -f vendor/composer/installed.json ]; then | |
| php -r '$data = json_decode(file_get_contents("vendor/composer/installed.json"), true, 512, JSON_THROW_ON_ERROR); foreach ($data["packages"] ?? $data as $package) { echo $package["name"], " ", $package["version"], PHP_EOL; }' > "$RUNNER_TEMP/es-optimizer-diagnostics/dependencies.txt" | |
| fi | |
| # Never upload config, authentication data, SQL, exports, or arbitrary logs. | |
| # Refuse oversized or linked diagnostics instead of uploading them. | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type l -delete | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type f -size +2M -delete | |
| - name: Upload diagnostic summary | |
| id: upload_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: wp-compat-plugin-check | |
| path: | | |
| ${{ runner.temp }}/es-optimizer-diagnostics/run.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/steps.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/php.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/dependencies.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/wordpress.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/junit.xml | |
| ${{ runner.temp }}/es-optimizer-diagnostics/package-manifest.txt | |
| ${{ runner.temp }}/enginescript-site-optimizer-candidate.zip | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| - name: Create issue on plugin check failure | |
| id: create_issue_on_plugin_check_failure | |
| if: ${{ failure() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' }} | |
| uses: JasonEtco/create-an-issue@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| FAILURE_STAGE: ${{ steps.checkout_code.outcome == 'failure' && 'checkout_code' || steps.setup_php_8_3.outcome == 'failure' && 'setup_php_8_3' || steps.clear_any_existing_composer_cache.outcome == 'failure' && 'clear_any_existing_composer_cache' || steps.install_composer_dependencies_no_cache.outcome == 'failure' && 'install_composer_dependencies_no_cache' || steps.clean_up_potential_artifact_directories.outcome == 'failure' && 'clean_up_potential_artifact_directories' || steps.prepare_plugin_check_build_directory.outcome == 'failure' && 'prepare_plugin_check_build_directory' || steps.wordpress_plugin_check.outcome == 'failure' && 'wordpress_plugin_check' || steps.handle_plugin_check_completion.outcome == 'failure' && 'handle_plugin_check_completion' || 'unknown_or_canceled' }} | |
| PHP_VERSION: '8.3' | |
| RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| filename: .github/ISSUE_TEMPLATE/plugin-check-failure.md | |
| update_existing: false | |
| - name: Mark job as failed after issue creation | |
| id: mark_job_as_failed_after_issue_creation | |
| if: ${{ failure() }} | |
| run: | | |
| echo "::error::Plugin Check job failed; see the failed step and reporting outcome." | |
| exit 1 | |
| phpcs: | |
| name: PHPCS (PHP 8.3) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@v7 | |
| - name: Setup PHP 8.3 | |
| id: setup_php_8_3 | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.3' | |
| extensions: mysqli, curl, zip, intl, gd, mbstring, fileinfo, xml | |
| coverage: none | |
| tools: composer:v2 | |
| - name: Install Composer dependencies | |
| id: install_composer_dependencies | |
| uses: ramsey/composer-install@v4 | |
| with: | |
| dependency-versions: highest | |
| composer-options: "--prefer-dist --no-progress" | |
| - name: Verify PHPCS standards | |
| id: verify_phpcs_standards | |
| run: | | |
| # Verify the project-local PHPCS installation and registered standards. | |
| vendor/bin/phpcs -i | |
| - name: Run PHPCS | |
| id: run_phpcs | |
| run: | | |
| vendor/bin/phpcs --standard=phpcs.xml | |
| # Static scanner controls; never execute these incompatible PHP fixtures. | |
| CONTROL_DIR="$RUNNER_TEMP/optimizer-compat-controls" | |
| mkdir -p "$CONTROL_DIR" "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| printf '<?php strlen("control");\n' > "$CONTROL_DIR/compatible.php" | |
| printf '<?php each(array());\n' > "$CONTROL_DIR/legacy-incompatible.php" | |
| printf '<?php json_validate("{}");\n' > "$CONTROL_DIR/modern-incompatible.php" | |
| vendor/bin/phpcs --standard=PHPCompatibilityWP --runtime-set testVersion 8.2 "$CONTROL_DIR/compatible.php" | |
| if vendor/bin/phpcs --standard=PHPCompatibilityWP --runtime-set testVersion 8.2 --report=json "$CONTROL_DIR/legacy-incompatible.php" > "$CONTROL_DIR/legacy.json"; then | |
| echo '::error::The known legacy-incompatible fixture was missed.' | |
| exit 1 | |
| else | |
| status=$? | |
| [[ $status == 1 || $status == 2 ]] | |
| fi | |
| php -r '$report = json_decode(file_get_contents($argv[1]), true, 512, JSON_THROW_ON_ERROR); foreach ($report["files"] ?? [] as $file) { foreach ($file["messages"] as $message) { if (str_starts_with($message["source"], "PHPCompatibility.") && str_contains($message["message"], "each")) { exit(0); } } } exit(1);' "$CONTROL_DIR/legacy.json" | |
| if vendor/bin/phpcs --standard=PHPCompatibilityWP --runtime-set testVersion 8.2 --report=json "$CONTROL_DIR/modern-incompatible.php" > "$CONTROL_DIR/modern.json"; then | |
| echo '::warning::CR-TOOL-001: current stable rules miss PHP 8.3 json_validate at the PHP 8.2 floor. Modern-rule acceptance remains pending.' | |
| echo 'modern_rule_coverage=known_gap' > "$RUNNER_TEMP/es-optimizer-diagnostics/compatibility-controls.txt" | |
| else | |
| status=$? | |
| [[ $status == 1 || $status == 2 ]] | |
| php -r '$report = json_decode(file_get_contents($argv[1]), true, 512, JSON_THROW_ON_ERROR); foreach ($report["files"] ?? [] as $file) { foreach ($file["messages"] as $message) { if (str_starts_with($message["source"], "PHPCompatibility.") && str_contains($message["message"], "json_validate")) { exit(0); } } } exit(1);' "$CONTROL_DIR/modern.json" | |
| echo 'modern_rule_coverage=fixture_detected' > "$RUNNER_TEMP/es-optimizer-diagnostics/compatibility-controls.txt" | |
| fi | |
| echo 'compatible=passed;legacy_incompatible=rejected' >> "$RUNNER_TEMP/es-optimizer-diagnostics/compatibility-controls.txt" | |
| - name: Preserve diagnostic summary | |
| id: preserve_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| env: | |
| STEP_RESULTS: | | |
| checkout_code=${{ steps.checkout_code.outcome }} | |
| setup_php_8_3=${{ steps.setup_php_8_3.outcome }} | |
| install_composer_dependencies=${{ steps.install_composer_dependencies.outcome }} | |
| verify_phpcs_standards=${{ steps.verify_phpcs_standards.outcome }} | |
| run_phpcs=${{ steps.run_phpcs.outcome }} | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| printf '%s\n' "$STEP_RESULTS" > "$RUNNER_TEMP/es-optimizer-diagnostics/steps.txt" | |
| printf 'commit=%s\njob=%s\n' "$GITHUB_SHA" "$GITHUB_JOB" > "$RUNNER_TEMP/es-optimizer-diagnostics/run.txt" | |
| if command -v php >/dev/null; then | |
| php -r 'echo PHP_VERSION, PHP_EOL;' > "$RUNNER_TEMP/es-optimizer-diagnostics/php.txt" | |
| fi | |
| if command -v composer >/dev/null && [ -f vendor/composer/installed.json ]; then | |
| php -r '$data = json_decode(file_get_contents("vendor/composer/installed.json"), true, 512, JSON_THROW_ON_ERROR); foreach ($data["packages"] ?? $data as $package) { echo $package["name"], " ", $package["version"], PHP_EOL; }' > "$RUNNER_TEMP/es-optimizer-diagnostics/dependencies.txt" | |
| fi | |
| # Never upload config, authentication data, SQL, exports, or arbitrary logs. | |
| # Refuse oversized or linked diagnostics instead of uploading them. | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type l -delete | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type f -size +2M -delete | |
| - name: Upload diagnostic summary | |
| id: upload_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: wp-compat-phpcs | |
| path: | | |
| ${{ runner.temp }}/es-optimizer-diagnostics/run.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/steps.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/php.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/dependencies.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/wordpress.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/junit.xml | |
| ${{ runner.temp }}/es-optimizer-diagnostics/compatibility-controls.txt | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| - name: Create issue on PHPCS failure | |
| id: create_issue_on_phpcs_failure | |
| if: ${{ failure() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' }} | |
| uses: JasonEtco/create-an-issue@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| FAILURE_STAGE: ${{ steps.checkout_code.outcome == 'failure' && 'checkout_code' || steps.setup_php_8_3.outcome == 'failure' && 'setup_php_8_3' || steps.install_composer_dependencies.outcome == 'failure' && 'install_composer_dependencies' || steps.verify_phpcs_standards.outcome == 'failure' && 'verify_phpcs_standards' || steps.run_phpcs.outcome == 'failure' && 'run_phpcs' || 'unknown_or_canceled' }} | |
| PHP_VERSION: '8.3' | |
| RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| filename: .github/ISSUE_TEMPLATE/phpcs-failure.md | |
| update_existing: false | |
| vip-phpcs: | |
| name: WP VIP CS (PHP 8.3) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@v7 | |
| - name: Setup PHP 8.3 | |
| id: setup_php_8_3 | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.3' | |
| extensions: mysqli, curl, zip, intl, gd, mbstring, fileinfo, xml | |
| coverage: none | |
| tools: composer:v2 | |
| - name: Install Composer dependencies | |
| id: install_composer_dependencies | |
| uses: ramsey/composer-install@v4 | |
| with: | |
| dependency-versions: highest | |
| composer-options: "--prefer-dist --no-progress" | |
| - name: Install WordPress VIP Coding Standards | |
| id: install_wordpress_vip_coding_standards | |
| run: | | |
| # Install PHPCS and WordPress VIP Coding Standards | |
| composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true | |
| composer global require --dev squizlabs/php_codesniffer:"^3.13.6" | |
| composer global require --dev wp-coding-standards/wpcs:"^3.4.1" | |
| composer global require --dev automattic/vipwpcs:"^3.0.1" | |
| composer global require --dev dealerdirect/phpcodesniffer-composer-installer:"^1.2" | |
| # Add composer bin to PATH | |
| VIP_BIN_DIR="$(composer global config bin-dir --absolute)" | |
| echo "$VIP_BIN_DIR" >> "$GITHUB_PATH" | |
| echo "VIP_PHPCS=$VIP_BIN_DIR/phpcs" >> "$GITHUB_ENV" | |
| export PATH="$VIP_BIN_DIR:$PATH" | |
| # Verify installation and show available standards | |
| "$VIP_BIN_DIR/phpcs" -i | |
| # Verify VIP standards are available | |
| "$VIP_BIN_DIR/phpcs" -i | grep -F WordPress-VIP-Go | |
| - name: Create VIP-specific PHPCS configuration | |
| id: create_vip_specific_phpcs_configuration | |
| run: | | |
| cat > phpcs-vip.xml << 'EOF' | |
| <?xml version="1.0"?> | |
| <ruleset name="WordPress VIP Go Coding Standards"> | |
| <description>WordPress VIP Go coding standards for enterprise-level WordPress development</description> | |
| <!-- Files to check --> | |
| <file>enginescript-site-optimizer.php</file> | |
| <file>includes</file> | |
| <file>uninstall.php</file> | |
| <!-- Exclude patterns --> | |
| <exclude-pattern>*/vendor/*</exclude-pattern> | |
| <exclude-pattern>*/node_modules/*</exclude-pattern> | |
| <exclude-pattern>*/tests/*</exclude-pattern> | |
| <exclude-pattern>*/assets/*</exclude-pattern> | |
| <exclude-pattern>*/languages/*</exclude-pattern> | |
| <exclude-pattern>*/.git/*</exclude-pattern> | |
| <exclude-pattern>*/.github/*</exclude-pattern> | |
| <exclude-pattern>*.js</exclude-pattern> | |
| <exclude-pattern>*.css</exclude-pattern> | |
| <!-- Use WordPress VIP Go coding standards --> | |
| <rule ref="WordPress-VIP-Go"> | |
| <!-- Allow short array syntax [] instead of array() --> | |
| <exclude name="Generic.Arrays.DisallowShortArraySyntax"/> | |
| <!-- Allow longer lines for readability in some cases --> | |
| <exclude name="Generic.Files.LineLength.TooLong"/> | |
| <!-- Exclude JS-related sniffs to avoid deprecation warnings --> | |
| <exclude name="WordPressVIPMinimum.JS"/> | |
| </rule> | |
| <!-- Add specific WordPressVIPMinimum rules (non-JS) --> | |
| <rule ref="WordPressVIPMinimum.Security"/> | |
| <rule ref="WordPressVIPMinimum.Performance"/> | |
| <rule ref="WordPressVIPMinimum.UserExperience"/> | |
| <rule ref="WordPressVIPMinimum.Functions"/> | |
| <rule ref="WordPressVIPMinimum.Variables"/> | |
| <rule ref="WordPressVIPMinimum.Files"/> | |
| <rule ref="WordPressVIPMinimum.Hooks"/> | |
| <!-- WordPress internationalization with plugin text domain --> | |
| <rule ref="WordPress.WP.I18n"> | |
| <properties> | |
| <property name="text_domain" type="array"> | |
| <element value="enginescript-site-optimizer"/> | |
| </property> | |
| </properties> | |
| </rule> | |
| <!-- Only scan PHP files --> | |
| <arg name="extensions" value="php"/> | |
| <!-- Show progress --> | |
| <arg value="p"/> | |
| <!-- Show sniff codes in all reports --> | |
| <arg value="s"/> | |
| <!-- Use colors in output --> | |
| <arg name="colors"/> | |
| <!-- Set report width for better readability --> | |
| <arg name="report-width" value="120"/> | |
| </ruleset> | |
| EOF | |
| - name: Run WordPress VIP PHPCS | |
| id: run_wordpress_vip_phpcs | |
| run: | | |
| echo "🚀 Running WordPress VIP Go Coding Standards scan..." | |
| echo "📋 This scan focuses on VIP-specific requirements including:" | |
| echo " • File system operation restrictions" | |
| echo " • Performance and caching best practices" | |
| echo " • Security vulnerabilities specific to VIP platform" | |
| echo " • User experience guidelines for enterprise WordPress" | |
| echo " • Uncached function usage patterns" | |
| echo "" | |
| echo "📦 Using standards: WordPress-VIP-Go, WordPressVIPMinimum" | |
| echo "🎯 Scanning PHP files only (excluding JS/CSS to avoid deprecation warnings)" | |
| echo "" | |
| # Run VIP-specific PHPCS scan using our custom configuration | |
| # Only scan PHP files to avoid JS/CSS deprecation warnings | |
| "$VIP_PHPCS" --standard=phpcs-vip.xml --extensions=php || { | |
| echo "" | |
| echo "⚠️ WordPress VIP coding standards issues found." | |
| echo "💡 Note: These are VIP-specific recommendations for enterprise WordPress platforms." | |
| echo "🔧 Many of these may not apply to standard WordPress installations." | |
| echo "📖 For more info: https://docs.wpvip.com/technical-references/code-quality-and-best-practices/" | |
| echo "" | |
| exit 1 | |
| } | |
| echo "✅ WordPress VIP coding standards check completed successfully!" | |
| - name: Preserve diagnostic summary | |
| id: preserve_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| env: | |
| STEP_RESULTS: | | |
| checkout_code=${{ steps.checkout_code.outcome }} | |
| setup_php_8_3=${{ steps.setup_php_8_3.outcome }} | |
| install_composer_dependencies=${{ steps.install_composer_dependencies.outcome }} | |
| install_wordpress_vip_coding_standards=${{ steps.install_wordpress_vip_coding_standards.outcome }} | |
| create_vip_specific_phpcs_configuration=${{ steps.create_vip_specific_phpcs_configuration.outcome }} | |
| run_wordpress_vip_phpcs=${{ steps.run_wordpress_vip_phpcs.outcome }} | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| printf '%s\n' "$STEP_RESULTS" > "$RUNNER_TEMP/es-optimizer-diagnostics/steps.txt" | |
| printf 'commit=%s\njob=%s\n' "$GITHUB_SHA" "$GITHUB_JOB" > "$RUNNER_TEMP/es-optimizer-diagnostics/run.txt" | |
| if command -v php >/dev/null; then | |
| php -r 'echo PHP_VERSION, PHP_EOL;' > "$RUNNER_TEMP/es-optimizer-diagnostics/php.txt" | |
| fi | |
| if command -v composer >/dev/null && [ -f vendor/composer/installed.json ]; then | |
| php -r '$data = json_decode(file_get_contents("vendor/composer/installed.json"), true, 512, JSON_THROW_ON_ERROR); foreach ($data["packages"] ?? $data as $package) { echo $package["name"], " ", $package["version"], PHP_EOL; }' > "$RUNNER_TEMP/es-optimizer-diagnostics/dependencies.txt" | |
| fi | |
| # Never upload config, authentication data, SQL, exports, or arbitrary logs. | |
| # Refuse oversized or linked diagnostics instead of uploading them. | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type l -delete | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type f -size +2M -delete | |
| - name: Upload diagnostic summary | |
| id: upload_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: wp-compat-vip-phpcs | |
| path: | | |
| ${{ runner.temp }}/es-optimizer-diagnostics/run.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/steps.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/php.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/dependencies.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/wordpress.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/junit.xml | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| - name: Create issue on VIP PHPCS failure | |
| id: create_issue_on_vip_phpcs_failure | |
| if: ${{ failure() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' }} | |
| uses: JasonEtco/create-an-issue@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| FAILURE_STAGE: ${{ steps.checkout_code.outcome == 'failure' && 'checkout_code' || steps.setup_php_8_3.outcome == 'failure' && 'setup_php_8_3' || steps.install_composer_dependencies.outcome == 'failure' && 'install_composer_dependencies' || steps.install_wordpress_vip_coding_standards.outcome == 'failure' && 'install_wordpress_vip_coding_standards' || steps.create_vip_specific_phpcs_configuration.outcome == 'failure' && 'create_vip_specific_phpcs_configuration' || steps.run_wordpress_vip_phpcs.outcome == 'failure' && 'run_wordpress_vip_phpcs' || 'unknown_or_canceled' }} | |
| PHP_VERSION: '8.3' | |
| RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| filename: .github/ISSUE_TEMPLATE/vip-phpcs-failure.md | |
| update_existing: false | |
| phpmd: | |
| name: PHPMD (PHP 8.3) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@v7 | |
| with: | |
| # Always fetch the latest commit, disable any caching | |
| fetch-depth: 0 | |
| clean: true | |
| - name: Verify latest commit | |
| id: verify_latest_commit | |
| run: | | |
| echo "=== Git Information ===" | |
| echo "Current commit: $(git rev-parse HEAD)" | |
| echo "Current branch: $(git rev-parse --abbrev-ref HEAD)" | |
| echo "Latest commit message: $(git log -1 --pretty=%B)" | |
| echo "=== End Git Information ===" | |
| - name: Setup PHP 8.3 | |
| id: setup_php_8_3 | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.3' | |
| extensions: mysqli, curl, zip, intl, gd, mbstring, fileinfo, xml | |
| coverage: none | |
| tools: composer:v2 | |
| - name: Clear any existing composer cache | |
| id: clear_any_existing_composer_cache | |
| run: | | |
| composer clear-cache || true | |
| rm -rf vendor/ composer.lock || true | |
| - name: Install Composer dependencies (no cache) | |
| id: install_composer_dependencies_no_cache | |
| run: | | |
| composer install --prefer-dist --no-progress --no-cache | |
| - name: Verify phpmd.xml content | |
| id: verify_phpmd_xml_content | |
| run: | | |
| echo "=== Current phpmd.xml content ===" | |
| cat phpmd.xml | |
| echo "=== End phpmd.xml content ===" | |
| - name: Run PHPMD | |
| id: run_phpmd | |
| run: | | |
| # Use WordPress-specific PHPMD configuration (WordPress snake_case compatible) | |
| echo "Using WordPress-specific PHPMD configuration (WordPress snake_case compatible)..." | |
| vendor/bin/phpmd enginescript-site-optimizer.php,includes,uninstall.php text phpmd.xml | |
| - name: Preserve diagnostic summary | |
| id: preserve_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| env: | |
| STEP_RESULTS: | | |
| checkout_code=${{ steps.checkout_code.outcome }} | |
| verify_latest_commit=${{ steps.verify_latest_commit.outcome }} | |
| setup_php_8_3=${{ steps.setup_php_8_3.outcome }} | |
| clear_any_existing_composer_cache=${{ steps.clear_any_existing_composer_cache.outcome }} | |
| install_composer_dependencies_no_cache=${{ steps.install_composer_dependencies_no_cache.outcome }} | |
| verify_phpmd_xml_content=${{ steps.verify_phpmd_xml_content.outcome }} | |
| run_phpmd=${{ steps.run_phpmd.outcome }} | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| printf '%s\n' "$STEP_RESULTS" > "$RUNNER_TEMP/es-optimizer-diagnostics/steps.txt" | |
| printf 'commit=%s\njob=%s\n' "$GITHUB_SHA" "$GITHUB_JOB" > "$RUNNER_TEMP/es-optimizer-diagnostics/run.txt" | |
| if command -v php >/dev/null; then | |
| php -r 'echo PHP_VERSION, PHP_EOL;' > "$RUNNER_TEMP/es-optimizer-diagnostics/php.txt" | |
| fi | |
| if command -v composer >/dev/null && [ -f vendor/composer/installed.json ]; then | |
| php -r '$data = json_decode(file_get_contents("vendor/composer/installed.json"), true, 512, JSON_THROW_ON_ERROR); foreach ($data["packages"] ?? $data as $package) { echo $package["name"], " ", $package["version"], PHP_EOL; }' > "$RUNNER_TEMP/es-optimizer-diagnostics/dependencies.txt" | |
| fi | |
| # Never upload config, authentication data, SQL, exports, or arbitrary logs. | |
| # Refuse oversized or linked diagnostics instead of uploading them. | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type l -delete | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type f -size +2M -delete | |
| - name: Upload diagnostic summary | |
| id: upload_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: wp-compat-phpmd | |
| path: | | |
| ${{ runner.temp }}/es-optimizer-diagnostics/run.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/steps.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/php.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/dependencies.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/wordpress.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/junit.xml | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| - name: Create issue on PHPMD failure | |
| id: create_issue_on_phpmd_failure | |
| if: ${{ failure() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' }} | |
| uses: JasonEtco/create-an-issue@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| FAILURE_STAGE: ${{ steps.checkout_code.outcome == 'failure' && 'checkout_code' || steps.verify_latest_commit.outcome == 'failure' && 'verify_latest_commit' || steps.setup_php_8_3.outcome == 'failure' && 'setup_php_8_3' || steps.clear_any_existing_composer_cache.outcome == 'failure' && 'clear_any_existing_composer_cache' || steps.install_composer_dependencies_no_cache.outcome == 'failure' && 'install_composer_dependencies_no_cache' || steps.verify_phpmd_xml_content.outcome == 'failure' && 'verify_phpmd_xml_content' || steps.run_phpmd.outcome == 'failure' && 'run_phpmd' || 'unknown_or_canceled' }} | |
| PHP_VERSION: '8.3' | |
| RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| filename: .github/ISSUE_TEMPLATE/phpmd-failure.md | |
| update_existing: false | |
| psalm-analysis: | |
| name: Psalm Static Analysis (PHP 8.3) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@v7 | |
| - name: Setup PHP 8.3 | |
| id: setup_php_8_3 | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.3' | |
| extensions: mysqli, curl, zip, intl, gd, mbstring, fileinfo, xml | |
| coverage: none | |
| tools: composer:v2 | |
| - name: Install Composer dependencies | |
| id: install_composer_dependencies | |
| uses: ramsey/composer-install@v4 | |
| with: | |
| dependency-versions: highest | |
| composer-options: "--prefer-dist --no-progress" | |
| - name: Verify Psalm installation | |
| id: verify_psalm_installation | |
| run: vendor/bin/psalm --version | |
| - name: Run Psalm | |
| id: run_psalm | |
| run: | | |
| vendor/bin/psalm --config=psalm.xml --show-info=true | |
| - name: Preserve diagnostic summary | |
| id: preserve_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| env: | |
| STEP_RESULTS: | | |
| checkout_code=${{ steps.checkout_code.outcome }} | |
| setup_php_8_3=${{ steps.setup_php_8_3.outcome }} | |
| install_composer_dependencies=${{ steps.install_composer_dependencies.outcome }} | |
| verify_psalm_installation=${{ steps.verify_psalm_installation.outcome }} | |
| run_psalm=${{ steps.run_psalm.outcome }} | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| printf '%s\n' "$STEP_RESULTS" > "$RUNNER_TEMP/es-optimizer-diagnostics/steps.txt" | |
| printf 'commit=%s\njob=%s\n' "$GITHUB_SHA" "$GITHUB_JOB" > "$RUNNER_TEMP/es-optimizer-diagnostics/run.txt" | |
| if command -v php >/dev/null; then | |
| php -r 'echo PHP_VERSION, PHP_EOL;' > "$RUNNER_TEMP/es-optimizer-diagnostics/php.txt" | |
| fi | |
| if command -v composer >/dev/null && [ -f vendor/composer/installed.json ]; then | |
| php -r '$data = json_decode(file_get_contents("vendor/composer/installed.json"), true, 512, JSON_THROW_ON_ERROR); foreach ($data["packages"] ?? $data as $package) { echo $package["name"], " ", $package["version"], PHP_EOL; }' > "$RUNNER_TEMP/es-optimizer-diagnostics/dependencies.txt" | |
| fi | |
| # Never upload config, authentication data, SQL, exports, or arbitrary logs. | |
| # Refuse oversized or linked diagnostics instead of uploading them. | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type l -delete | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type f -size +2M -delete | |
| - name: Upload diagnostic summary | |
| id: upload_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: wp-compat-psalm-analysis | |
| path: | | |
| ${{ runner.temp }}/es-optimizer-diagnostics/run.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/steps.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/php.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/dependencies.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/wordpress.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/junit.xml | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| - name: Create issue on Psalm failure | |
| id: create_issue_on_psalm_failure | |
| if: ${{ failure() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' }} | |
| uses: JasonEtco/create-an-issue@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| FAILURE_STAGE: ${{ steps.checkout_code.outcome == 'failure' && 'checkout_code' || steps.setup_php_8_3.outcome == 'failure' && 'setup_php_8_3' || steps.install_composer_dependencies.outcome == 'failure' && 'install_composer_dependencies' || steps.verify_psalm_installation.outcome == 'failure' && 'verify_psalm_installation' || steps.run_psalm.outcome == 'failure' && 'run_psalm' || 'unknown_or_canceled' }} | |
| PHP_VERSION: '8.3' | |
| RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| filename: .github/ISSUE_TEMPLATE/psalm-failure.md | |
| update_existing: false | |
| security-check: | |
| name: Security Scan (PHP 8.3) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@v7 | |
| - name: Setup PHP 8.3 | |
| id: setup_php_8_3 | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.3' | |
| extensions: mysqli, curl, zip, intl, gd, mbstring, fileinfo, xml | |
| coverage: none | |
| tools: composer:v2 | |
| - name: Install Composer dependencies | |
| id: install_composer_dependencies | |
| uses: ramsey/composer-install@v4 | |
| with: | |
| dependency-versions: highest | |
| composer-options: "--prefer-dist --no-progress" | |
| - name: Security Check for known vulnerabilities in dependencies | |
| id: security_check_for_known_vulnerabilities_in_dependencies | |
| uses: symfonycorp/security-checker-action@v5 | |
| - name: WordPress Security Scan | |
| id: wordpress_security_scan | |
| run: | | |
| echo "Performing WordPress plugin security analysis..." | |
| # Basic security pattern checks for common WordPress vulnerabilities | |
| echo "Checking for common security issues..." | |
| security_findings=0 | |
| test -f enginescript-site-optimizer.php | |
| test -f uninstall.php | |
| for source in includes/admin.php includes/bootstrap.php includes/frontend.php includes/options.php; do | |
| test -f "$source" | |
| done | |
| grep_php() { | |
| if grep -n "$1" enginescript-site-optimizer.php includes/*.php uninstall.php; then | |
| return 0 | |
| else | |
| local scan_status=$? | |
| if [ "$scan_status" -gt 1 ]; then | |
| echo '::error::Security scan could not read its production inputs.' >&2 | |
| exit "$scan_status" | |
| fi | |
| return 1 | |
| fi | |
| } | |
| # Check for potential SQL injection patterns | |
| if grep_php "mysql_query\|mysqli_query"; then | |
| echo "::error::Direct database queries found. Ensure proper sanitization." | |
| security_findings=1 | |
| fi | |
| # Check for potential XSS vulnerabilities (missing escaping) | |
| if grep_php "echo \$_\|print \$_"; then | |
| echo "::error::Potential XSS vulnerability found. Ensure output is escaped." | |
| security_findings=1 | |
| fi | |
| # Check for file inclusion vulnerabilities | |
| if grep_php "include.*\$_\|require.*\$_"; then | |
| echo "::error::Potential file inclusion vulnerability found." | |
| security_findings=1 | |
| fi | |
| # Check for eval() usage (security risk) | |
| if grep_php "eval("; then | |
| echo "::error::eval() function usage detected. This is a security risk." | |
| security_findings=1 | |
| fi | |
| # Check for proper nonce usage | |
| if grep_php "wp_nonce_field\|wp_verify_nonce\|settings_fields" >/dev/null 2>&1; then | |
| echo "WordPress nonce usage detected." | |
| else | |
| echo "Info: Consider adding WordPress nonces for form security." | |
| fi | |
| # Check for proper sanitization functions | |
| if grep_php "sanitize_\|esc_" >/dev/null 2>&1; then | |
| echo "WordPress sanitization functions detected." | |
| else | |
| echo "::error::Limited use of WordPress sanitization functions." | |
| security_findings=1 | |
| fi | |
| # Check for capability checks | |
| if grep_php "current_user_can\|user_can" >/dev/null 2>&1; then | |
| echo "WordPress capability checks detected." | |
| else | |
| echo "Info: Consider adding user capability checks where appropriate." | |
| fi | |
| if [ "$security_findings" -ne 0 ]; then | |
| echo "::error::WordPress security scan found blocking findings." | |
| exit 1 | |
| fi | |
| echo "WordPress security scan completed." | |
| - name: Preserve diagnostic summary | |
| id: preserve_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| env: | |
| STEP_RESULTS: | | |
| checkout_code=${{ steps.checkout_code.outcome }} | |
| setup_php_8_3=${{ steps.setup_php_8_3.outcome }} | |
| install_composer_dependencies=${{ steps.install_composer_dependencies.outcome }} | |
| security_check_for_known_vulnerabilities_in_dependencies=${{ steps.security_check_for_known_vulnerabilities_in_dependencies.outcome }} | |
| wordpress_security_scan=${{ steps.wordpress_security_scan.outcome }} | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| printf '%s\n' "$STEP_RESULTS" > "$RUNNER_TEMP/es-optimizer-diagnostics/steps.txt" | |
| printf 'commit=%s\njob=%s\n' "$GITHUB_SHA" "$GITHUB_JOB" > "$RUNNER_TEMP/es-optimizer-diagnostics/run.txt" | |
| if command -v php >/dev/null; then | |
| php -r 'echo PHP_VERSION, PHP_EOL;' > "$RUNNER_TEMP/es-optimizer-diagnostics/php.txt" | |
| fi | |
| if command -v composer >/dev/null && [ -f vendor/composer/installed.json ]; then | |
| php -r '$data = json_decode(file_get_contents("vendor/composer/installed.json"), true, 512, JSON_THROW_ON_ERROR); foreach ($data["packages"] ?? $data as $package) { echo $package["name"], " ", $package["version"], PHP_EOL; }' > "$RUNNER_TEMP/es-optimizer-diagnostics/dependencies.txt" | |
| fi | |
| # Never upload config, authentication data, SQL, exports, or arbitrary logs. | |
| # Refuse oversized or linked diagnostics instead of uploading them. | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type l -delete | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type f -size +2M -delete | |
| - name: Upload diagnostic summary | |
| id: upload_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: wp-compat-security-check | |
| path: | | |
| ${{ runner.temp }}/es-optimizer-diagnostics/run.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/steps.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/php.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/dependencies.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/wordpress.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/junit.xml | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| - name: Create issue on security vulnerability | |
| id: create_issue_on_security_vulnerability | |
| if: ${{ failure() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' }} | |
| uses: JasonEtco/create-an-issue@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| FAILURE_STAGE: ${{ steps.checkout_code.outcome == 'failure' && 'checkout_code' || steps.setup_php_8_3.outcome == 'failure' && 'setup_php_8_3' || steps.install_composer_dependencies.outcome == 'failure' && 'install_composer_dependencies' || steps.security_check_for_known_vulnerabilities_in_dependencies.outcome == 'failure' && 'security_check_for_known_vulnerabilities_in_dependencies' || steps.wordpress_security_scan.outcome == 'failure' && 'wordpress_security_scan' || 'unknown_or_canceled' }} | |
| PHP_VERSION: '8.3' | |
| RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| filename: .github/ISSUE_TEMPLATE/security-failure.md | |
| update_existing: false | |
| phpstan-wordpress: | |
| name: PHPStan for WP (PHP 8.3) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@v7 | |
| with: | |
| # Always fetch the latest commit, disable any caching | |
| fetch-depth: 0 | |
| clean: true | |
| - name: Setup PHP 8.3 | |
| id: setup_php_8_3 | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.3' | |
| extensions: mysqli, curl, zip, intl, gd, mbstring, fileinfo, xml | |
| coverage: none | |
| tools: composer:v2 | |
| - name: Clear any existing composer cache | |
| id: clear_any_existing_composer_cache | |
| run: | | |
| composer clear-cache || true | |
| rm -rf vendor/ composer.lock || true | |
| - name: Install Composer dependencies (no cache) | |
| id: install_composer_dependencies_no_cache | |
| run: | | |
| composer install --prefer-dist --no-progress --no-cache | |
| - name: Verify phpstan.neon content | |
| id: verify_phpstan_neon_content | |
| run: | | |
| echo "=== Current phpstan.neon content ===" | |
| cat phpstan.neon | |
| echo "=== End phpstan.neon content ===" | |
| - name: PHPStan for WordPress Analysis | |
| id: phpstan_for_wordpress_analysis | |
| run: | | |
| echo "Running PHPStan analysis with WordPress stubs..." | |
| vendor/bin/phpstan analyse --no-progress --error-format=table | |
| echo "✅ PHPStan analysis completed successfully!" | |
| - name: Preserve diagnostic summary | |
| id: preserve_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| env: | |
| STEP_RESULTS: | | |
| checkout_code=${{ steps.checkout_code.outcome }} | |
| setup_php_8_3=${{ steps.setup_php_8_3.outcome }} | |
| clear_any_existing_composer_cache=${{ steps.clear_any_existing_composer_cache.outcome }} | |
| install_composer_dependencies_no_cache=${{ steps.install_composer_dependencies_no_cache.outcome }} | |
| verify_phpstan_neon_content=${{ steps.verify_phpstan_neon_content.outcome }} | |
| phpstan_for_wordpress_analysis=${{ steps.phpstan_for_wordpress_analysis.outcome }} | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| printf '%s\n' "$STEP_RESULTS" > "$RUNNER_TEMP/es-optimizer-diagnostics/steps.txt" | |
| printf 'commit=%s\njob=%s\n' "$GITHUB_SHA" "$GITHUB_JOB" > "$RUNNER_TEMP/es-optimizer-diagnostics/run.txt" | |
| if command -v php >/dev/null; then | |
| php -r 'echo PHP_VERSION, PHP_EOL;' > "$RUNNER_TEMP/es-optimizer-diagnostics/php.txt" | |
| fi | |
| if command -v composer >/dev/null && [ -f vendor/composer/installed.json ]; then | |
| php -r '$data = json_decode(file_get_contents("vendor/composer/installed.json"), true, 512, JSON_THROW_ON_ERROR); foreach ($data["packages"] ?? $data as $package) { echo $package["name"], " ", $package["version"], PHP_EOL; }' > "$RUNNER_TEMP/es-optimizer-diagnostics/dependencies.txt" | |
| fi | |
| # Never upload config, authentication data, SQL, exports, or arbitrary logs. | |
| # Refuse oversized or linked diagnostics instead of uploading them. | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type l -delete | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type f -size +2M -delete | |
| - name: Upload diagnostic summary | |
| id: upload_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: wp-compat-phpstan-wordpress | |
| path: | | |
| ${{ runner.temp }}/es-optimizer-diagnostics/run.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/steps.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/php.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/dependencies.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/wordpress.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/junit.xml | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| - name: Create issue on PHPStan failure | |
| id: create_issue_on_phpstan_failure | |
| if: ${{ failure() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' }} | |
| uses: JasonEtco/create-an-issue@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| FAILURE_STAGE: ${{ steps.checkout_code.outcome == 'failure' && 'checkout_code' || steps.setup_php_8_3.outcome == 'failure' && 'setup_php_8_3' || steps.clear_any_existing_composer_cache.outcome == 'failure' && 'clear_any_existing_composer_cache' || steps.install_composer_dependencies_no_cache.outcome == 'failure' && 'install_composer_dependencies_no_cache' || steps.verify_phpstan_neon_content.outcome == 'failure' && 'verify_phpstan_neon_content' || steps.phpstan_for_wordpress_analysis.outcome == 'failure' && 'phpstan_for_wordpress_analysis' || 'unknown_or_canceled' }} | |
| PHP_VERSION: '8.3' | |
| RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| filename: .github/ISSUE_TEMPLATE/phpstan-failure.md | |
| update_existing: false | |
| wp-version-test: | |
| name: Test WordPress ${{ matrix.wp-version }} with PHP ${{ matrix.php-version }} (${{ matrix.dependency-versions }} deps) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| strategy: | |
| matrix: | |
| php-version: ['8.2', '8.3', '8.4', '8.5'] | |
| wp-version: ['6.8', 'latest', 'nightly'] | |
| dependency-versions: ['highest'] | |
| include: | |
| - php-version: '8.2' | |
| wp-version: 'latest' | |
| dependency-versions: 'lowest' | |
| fail-fast: false | |
| services: | |
| mysql: | |
| image: mysql:8.4 | |
| env: | |
| MYSQL_ROOT_PASSWORD: root | |
| MYSQL_DATABASE: wordpress_test | |
| ports: | |
| - 3306:3306 | |
| options: --health-cmd="mysqladmin ping -proot --silent" --health-interval=10s --health-timeout=5s --health-retries=3 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@v7 | |
| - name: Setup PHP ${{ matrix.php-version }} | |
| id: setup_php | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: ${{ matrix.php-version }} | |
| extensions: mysqli, curl, zip, intl, gd, mbstring, fileinfo, xml | |
| coverage: none | |
| tools: composer:v2 | |
| - name: Use isolated Composer auth | |
| id: use_isolated_composer_auth | |
| run: | | |
| # Avoid inheriting a masked or malformed GitHub OAuth token from Composer auth. | |
| composer_home="${RUNNER_TEMP}/composer-home" | |
| composer_cache="${RUNNER_TEMP}/composer-cache" | |
| mkdir -p "$composer_home" "$composer_cache" | |
| { | |
| echo "COMPOSER_HOME=$composer_home" | |
| echo "COMPOSER_CACHE_DIR=$composer_cache" | |
| echo 'COMPOSER_AUTH={"github-oauth":{"github.com":"${{ github.token }}"}}' | |
| } >> "$GITHUB_ENV" | |
| - name: PHP syntax lint | |
| id: php_syntax_lint | |
| run: | | |
| find . \ | |
| -path './vendor' -prune -o \ | |
| -path './tests' -prune -o \ | |
| -path './build' -prune -o \ | |
| -path './plugin-check-build' -prune -o \ | |
| -name '*.php' -print0 | xargs -0 -n1 php -l | |
| - name: Install Subversion | |
| id: install_subversion | |
| timeout-minutes: 15 | |
| run: sudo apt-get update && sudo apt-get install -y subversion | |
| - name: Install Composer dependencies | |
| id: install_composer_dependencies | |
| uses: ramsey/composer-install@v4 | |
| with: | |
| dependency-versions: ${{ matrix.dependency-versions }} | |
| composer-options: "--prefer-dist --no-progress" | |
| - name: Run isolated unit tests | |
| id: run_isolated_unit_tests | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| php -r '$data = json_decode(file_get_contents("vendor/composer/installed.json"), true, 512, JSON_THROW_ON_ERROR); foreach ($data["packages"] as $package) { echo $package["name"], " ", $package["version"], PHP_EOL; }' > "$RUNNER_TEMP/es-optimizer-diagnostics/unit-dependencies.txt" | |
| composer test -- --log-junit "$RUNNER_TEMP/es-optimizer-diagnostics/unit-junit.xml" | |
| if composer test -- --do-not-cache-result --filter '__es_optimizer_intentionally_no_matching_test__' > "$RUNNER_TEMP/optimizer-unit-empty.txt" 2>&1; then | |
| echo '::error::An empty unit suite incorrectly succeeded.' | |
| exit 1 | |
| else | |
| status=$? | |
| [[ $status == 1 ]] | |
| grep -q 'No tests executed' "$RUNNER_TEMP/optimizer-unit-empty.txt" | |
| fi | |
| cat > "$RUNNER_TEMP/OptimizerUnitFailureTest.php" <<'EOF' | |
| <?php | |
| final class OptimizerUnitFailureTest extends PHPUnit\Framework\TestCase { | |
| public function test_intentional_failure(): void { | |
| $this->assertTrue(false, 'Optimizer unit assertion control'); | |
| } | |
| } | |
| EOF | |
| if vendor/bin/phpunit --no-configuration --do-not-cache-result "$RUNNER_TEMP/OptimizerUnitFailureTest.php" > "$RUNNER_TEMP/optimizer-unit-failure.txt" 2>&1; then | |
| echo '::error::An intentionally failing unit assertion succeeded.' | |
| exit 1 | |
| else | |
| status=$? | |
| [[ $status == 1 ]] | |
| grep -q 'Optimizer unit assertion control' "$RUNNER_TEMP/optimizer-unit-failure.txt" | |
| fi | |
| echo 'Unit nonempty and assertion-failure controls passed.' > "$RUNNER_TEMP/es-optimizer-diagnostics/unit-controls.txt" | |
| - name: Pin PHPUnit for WordPress test suite | |
| id: pin_phpunit_for_wordpress_test_suite | |
| run: | | |
| # The WordPress test library still calls PHPUnit APIs removed in PHPUnit 10+. | |
| # Keep this generated integration-test job on PHPUnit 9.6 while the rest | |
| # of the project can continue using newer Composer-resolved QA tooling. | |
| if [ "${{ matrix.dependency-versions }}" = "lowest" ]; then | |
| composer require --dev phpunit/phpunit:"^9.6" yoast/phpunit-polyfills:"^4.0" --with-all-dependencies --prefer-lowest --no-progress | |
| else | |
| composer require --dev phpunit/phpunit:"^9.6" yoast/phpunit-polyfills:"^4.0" --with-all-dependencies --no-progress | |
| fi | |
| - name: Verify PHPUnit dependency versions | |
| id: verify_phpunit_dependency_versions | |
| run: | | |
| composer show phpunit/phpunit | |
| composer show yoast/phpunit-polyfills | |
| vendor/bin/phpunit --version | grep -E '^PHPUnit 9\.6\.' | |
| - name: Prepare Database | |
| id: prepare_database | |
| run: | | |
| mysql -u root --password=root --host=127.0.0.1 --port=3306 -e "DROP DATABASE IF EXISTS wordpress_test;" | |
| mysqladmin -u root --password=root --host=127.0.0.1 --port=3306 --force create wordpress_test | |
| - name: Create tests directory structure | |
| id: create_tests_directory_structure | |
| run: | | |
| mkdir -p tests/bin | |
| mkdir -p tests/bootstrap | |
| - name: Create WP tests install script | |
| id: create_wp_tests_install_script | |
| run: | | |
| cat > tests/bin/install-wp-tests.sh << 'EOF' | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| set +x | |
| umask 077 | |
| if [ $# -lt 3 ]; then | |
| echo "usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version] [skip-database-creation]" | |
| exit 1 | |
| fi | |
| DB_NAME=$1 | |
| DB_USER=$2 | |
| DB_PASS=$3 | |
| DB_HOST=${4-localhost} | |
| WP_VERSION=${5-latest} | |
| SKIP_DB_CREATE=${6-false} | |
| WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} | |
| WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} | |
| # Only the isolated GitHub service database is in scope. | |
| [[ $DB_NAME == wordpress_test && $DB_HOST == 127.0.0.1:3306 ]] | |
| [[ $SKIP_DB_CREATE == true || $SKIP_DB_CREATE == false ]] | |
| download() { | |
| curl --fail --silent --show-error --location \ | |
| --proto '=https' --proto-redir '=https' \ | |
| --connect-timeout 20 --max-time 180 --retry 3 \ | |
| "$1" --output "$2" | |
| test -s "$2" | |
| } | |
| if [[ $WP_VERSION == latest ]]; then | |
| download https://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json | |
| WP_VERSION=$(php -r ' | |
| $data = json_decode(file_get_contents($argv[1]), true, 512, JSON_THROW_ON_ERROR); | |
| $versions = []; | |
| foreach ($data["offers"] ?? [] as $offer) { | |
| if (($offer["response"] ?? null) !== "upgrade") { | |
| continue; | |
| } | |
| $version = $offer["current"] ?? null; | |
| if (!is_string($version) || !preg_match("/\A[0-9]+\.[0-9]+(?:\.[0-9]+)?\z/", $version)) { | |
| throw new RuntimeException("Invalid stable WordPress offer."); | |
| } | |
| $versions[$version] = true; | |
| } | |
| if (count($versions) !== 1) { | |
| throw new RuntimeException("Expected exactly one stable WordPress release."); | |
| } | |
| echo array_key_first($versions); | |
| ' /tmp/wp-latest.json) | |
| fi | |
| if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then | |
| WP_TESTS_TAG="tags/$WP_VERSION" | |
| elif [[ $WP_VERSION == nightly || $WP_VERSION == trunk ]]; then | |
| WP_TESTS_TAG=trunk | |
| else | |
| echo "Invalid WordPress version selector" >&2 | |
| exit 1 | |
| fi | |
| WP_SOURCE_URL="https://develop.svn.wordpress.org/$WP_TESTS_TAG" | |
| WP_REVISION=$(svn info --non-interactive --show-item revision "$WP_SOURCE_URL") | |
| [[ $WP_REVISION =~ ^[0-9]+$ ]] | |
| install_wp() { | |
| # Do not silently reuse a partial install. | |
| [[ ! -e $WP_CORE_DIR && ! -L $WP_CORE_DIR ]] | |
| if [[ $WP_TESTS_TAG == trunk ]]; then | |
| # Nightly core and test fixtures use one immutable SVN snapshot. | |
| svn export --non-interactive --quiet -r "$WP_REVISION" \ | |
| "$WP_SOURCE_URL/src@$WP_REVISION" "$WP_CORE_DIR" | |
| else | |
| mkdir -p "$WP_CORE_DIR" | |
| download "https://wordpress.org/wordpress-$WP_VERSION.tar.gz" /tmp/wordpress.tar.gz | |
| tar --strip-components=1 -zxf /tmp/wordpress.tar.gz -C "$WP_CORE_DIR" | |
| fi | |
| # setup-php supplies native mysqli; never fetch a mutable third-party db.php. | |
| php -r 'exit(extension_loaded("mysqli") ? 0 : 1);' | |
| test ! -e "$WP_CORE_DIR/wp-content/db.php" | |
| CORE_VERSION=$(php -r 'require $argv[1]; echo $wp_version;' "$WP_CORE_DIR/wp-includes/version.php") | |
| SOURCE_VERSION=$(svn cat --non-interactive -r "$WP_REVISION" \ | |
| "$WP_SOURCE_URL/src/wp-includes/version.php@$WP_REVISION" | | |
| php -r '$source = stream_get_contents(STDIN); if (!preg_match("/\\\$wp_version\s*=\s*\x27([^\x27]+)\x27/", $source, $match)) { exit(1); } echo $match[1];') | |
| [[ ${CORE_VERSION%-src} == "${SOURCE_VERSION%-src}" ]] | |
| if [[ $WP_TESTS_TAG != trunk ]]; then | |
| [[ ${CORE_VERSION%-src} == "$WP_VERSION" ]] | |
| fi | |
| mkdir -p "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| printf 'core=%s\nsource=%s\nrevision=%s\n' "$CORE_VERSION" "$WP_TESTS_TAG" "$WP_REVISION" \ | |
| > "$RUNNER_TEMP/es-optimizer-diagnostics/wordpress.txt" | |
| } | |
| install_test_suite() { | |
| [[ ! -e $WP_TESTS_DIR && ! -L $WP_TESTS_DIR ]] | |
| mkdir -p "$WP_TESTS_DIR" | |
| svn co --non-interactive --quiet -r "$WP_REVISION" \ | |
| "$WP_SOURCE_URL/tests/phpunit/includes@$WP_REVISION" "$WP_TESTS_DIR/includes" | |
| svn co --non-interactive --quiet -r "$WP_REVISION" \ | |
| "$WP_SOURCE_URL/tests/phpunit/data@$WP_REVISION" "$WP_TESTS_DIR/data" | |
| if [ ! -f "$WP_TESTS_DIR/wp-tests-config.php" ]; then | |
| svn cat --non-interactive -r "$WP_REVISION" \ | |
| "$WP_SOURCE_URL/wp-tests-config-sample.php@$WP_REVISION" \ | |
| > "$WP_TESTS_DIR/wp-tests-config.php" | |
| export WP_TESTS_DIR WP_CORE_DIR DB_NAME DB_USER DB_PASS DB_HOST | |
| # Quote PHP literals safely; do not interpolate credentials through sed. | |
| php -r ' | |
| $path = getenv("WP_TESTS_DIR") . "/wp-tests-config.php"; | |
| $config = file_get_contents($path); | |
| $replacements = [ | |
| "dirname( __FILE__ ) . \x27/src/\x27" => rtrim(getenv("WP_CORE_DIR"), "/") . "/", | |
| "\x27youremptytestdbnamehere\x27" => getenv("DB_NAME"), | |
| "\x27yourusernamehere\x27" => getenv("DB_USER"), | |
| "\x27yourpasswordhere\x27" => getenv("DB_PASS"), | |
| "\x27localhost\x27" => getenv("DB_HOST"), | |
| ]; | |
| foreach ($replacements as $needle => $value) { | |
| if (substr_count($config, $needle) !== 1) { | |
| throw new RuntimeException("Unexpected WordPress test config template."); | |
| } | |
| $config = str_replace($needle, var_export($value, true), $config); | |
| } | |
| if (file_put_contents($path, $config) !== strlen($config)) { | |
| throw new RuntimeException("Cannot write WordPress test config."); | |
| } | |
| ' | |
| fi | |
| } | |
| install_db() { | |
| if [[ $SKIP_DB_CREATE == true ]]; then | |
| return | |
| fi | |
| # Fallback is limited to this job's disposable service and never drops a DB. | |
| MYSQL_PWD="$DB_PASS" mysql --user="$DB_USER" --host=127.0.0.1 --port=3306 \ | |
| -e 'CREATE DATABASE wordpress_test' | |
| } | |
| install_wp | |
| install_test_suite | |
| install_db | |
| EOF | |
| chmod +x tests/bin/install-wp-tests.sh | |
| - name: Create Bootstrap File | |
| id: create_bootstrap_file | |
| run: | | |
| mkdir -p tests | |
| cat > tests/bootstrap.php << 'EOF' | |
| <?php | |
| /** Native bootstrap generated on the runner; no workstation doubles. */ | |
| if ('true' !== getenv('GITHUB_ACTIONS')) { | |
| throw new RuntimeException('Native tests require the isolated GitHub runner.'); | |
| } | |
| define('WP_TESTS_PHPUNIT_POLYFILLS_PATH', dirname(__DIR__) . '/vendor/yoast/phpunit-polyfills'); | |
| require_once '/tmp/wordpress-tests-lib/includes/functions.php'; | |
| tests_add_filter('muplugins_loaded', static function () { | |
| require dirname(__DIR__) . '/enginescript-site-optimizer.php'; | |
| }); | |
| require '/tmp/wordpress-tests-lib/includes/bootstrap.php'; | |
| // Load the exact uninstall entry once on the empty disposable installation. | |
| // Later helper tests cover DB/cache/failure semantics, not core file deletion. | |
| define('WP_UNINSTALL_PLUGIN', 'enginescript-site-optimizer/enginescript-site-optimizer.php'); | |
| require dirname(__DIR__) . '/uninstall.php'; | |
| EOF | |
| - name: Create Test File | |
| id: create_test_file | |
| run: | | |
| cat > tests/EngineScriptSiteOptimizerTest.php << 'EOF' | |
| <?php | |
| /** Native WordPress regression cases; generated and run only on fresh CI runners. */ | |
| final class EngineScriptSiteOptimizerTest extends WP_UnitTestCase { | |
| /** | |
| * Reset native state for each regression. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function set_up() { | |
| parent::set_up(); | |
| require_once ABSPATH . 'wp-admin/includes/plugin.php'; | |
| require_once ABSPATH . 'wp-admin/includes/template.php'; | |
| wp_set_current_user(0); | |
| delete_option('es_optimizer_options'); | |
| delete_transient('settings_errors'); | |
| $GLOBALS['wp_settings_errors'] = array(); | |
| es_optimizer_init_settings(); | |
| } | |
| /** Store a legacy fixture without changing production sanitization. */ | |
| private function legacy_options(array $options): void { | |
| remove_filter('sanitize_option_es_optimizer_options', 'es_optimizer_validate_options'); | |
| try { | |
| update_option('es_optimizer_options', $options); | |
| } finally { | |
| add_filter('sanitize_option_es_optimizer_options', 'es_optimizer_validate_options'); | |
| } | |
| } | |
| /** Inspect the physical row separately from core default/notoptions caches. */ | |
| private function option_rows(): int { | |
| global $wpdb; | |
| return (int) $wpdb->get_var($wpdb->prepare( | |
| 'SELECT COUNT(*) FROM %i WHERE option_name = %s', $wpdb->options, 'es_optimizer_options' | |
| )); | |
| } | |
| /** | |
| * Require the dedicated multisite invocation. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| private function require_multisite(): void { | |
| if (!is_multisite()) { | |
| $this->markTestSkipped('Executed by the separate multisite invocation in this same matrix cell.'); | |
| } | |
| } | |
| /** | |
| * Native bootstrap mode and optimizer hooks. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_native_bootstrap_mode_and_optimizer_hooks(): void { | |
| $this->assertSame('1' === getenv('WP_MULTISITE'), is_multisite()); | |
| $this->assertSame(get_plugin_data(ES_SITE_OPTIMIZER_FILE, false, false)['Version'], ES_SITE_OPTIMIZER_VERSION); | |
| $this->assertSame(10, has_filter('wp_resource_hints', 'es_optimizer_add_preconnect_resource_hints')); | |
| $this->assertSame(10, has_filter('tiny_mce_plugins', 'es_optimizer_disable_emojis_tinymce')); | |
| $this->assertCount(13, es_optimizer_get_settings_fields()); | |
| $this->assertCount(11, es_optimizer_get_boolean_option_keys()); | |
| } | |
| /** | |
| * Virtual defaults activation and deactivation. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_virtual_defaults_activation_and_deactivation(): void { | |
| es_optimizer_activate_plugin(is_multisite()); | |
| $this->assertSame(es_optimizer_get_default_options(), es_optimizer_get_options()); | |
| $this->assertSame(0, $this->option_rows()); | |
| $this->assertFalse(update_option('es_optimizer_options', es_optimizer_get_default_options())); | |
| $this->assertSame(0, $this->option_rows()); | |
| $saved = array_merge(es_optimizer_get_default_options(), array('preconnect_domains' => 'https://saved.example.com')); | |
| update_option('es_optimizer_options', $saved); | |
| es_optimizer_deactivate_plugin(is_multisite()); | |
| $this->assertSame($saved, get_option('es_optimizer_options')); | |
| $this->assertSame(1, $this->option_rows()); | |
| } | |
| /** | |
| * Native add update delete without manual refresh. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_native_add_update_delete_without_manual_refresh(): void { | |
| $this->assertSame(0, $this->option_rows()); | |
| $saved = array_merge(es_optimizer_get_default_options(), array('preconnect_domains' => 'https://first.example.com')); | |
| $this->assertTrue(add_option('es_optimizer_options', $saved)); | |
| $this->assertSame($saved, es_optimizer_get_options()); | |
| $saved['preconnect_domains'] = 'https://second.example.com'; | |
| $this->assertTrue(update_option('es_optimizer_options', $saved)); | |
| $this->assertSame($saved, es_optimizer_get_options()); | |
| delete_option('es_optimizer_options'); | |
| $this->assertSame(es_optimizer_get_default_options(), es_optimizer_get_options()); | |
| $this->assertSame(0, $this->option_rows()); | |
| } | |
| /** | |
| * Setting registration and rest exclusion. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_setting_registration_and_rest_exclusion(): void { | |
| $setting = get_registered_settings()['es_optimizer_options']; | |
| $this->assertSame('es_optimizer_settings', $setting['group']); | |
| $this->assertSame('es_optimizer_validate_options', $setting['sanitize_callback']); | |
| $this->assertFalse($setting['show_in_rest']); | |
| $controller = new WP_REST_Settings_Controller(); | |
| $this->assertArrayNotHasKey('es_optimizer_options', $controller->get_item_schema()['properties']); | |
| } | |
| /** | |
| * Native first insert safe notice and blank save. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_native_first_insert_safe_notice_and_blank_save(): void { | |
| $saved = array('preconnect_domains' => "https://user:synthetic-token@example.com\nhttps://ok.example.com"); | |
| $this->assertTrue(update_option('es_optimizer_options', $saved)); | |
| $this->assertSame('https://ok.example.com', get_option('es_optimizer_options')['preconnect_domains']); | |
| $warnings = get_settings_errors('es_optimizer_options'); | |
| $this->assertCount(1, $warnings); | |
| $this->assertStringNotContainsString('synthetic-token', serialize($warnings)); | |
| $this->assertStringNotContainsString('user:', serialize($warnings)); | |
| update_option('es_optimizer_options', array('preconnect_domains' => '')); | |
| $this->assertSame('', get_option('es_optimizer_options')['preconnect_domains']); | |
| } | |
| /** | |
| * Native invalid fields and budgets preserve prior field. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_native_invalid_fields_and_budgets_preserve_prior_field(): void { | |
| foreach (array('preconnect_domains' => 'dns_prefetch_domains', 'dns_prefetch_domains' => 'preconnect_domains') as $field => $other) { | |
| update_option('es_optimizer_options', array($field => 'https://prior.example.com')); | |
| foreach (array(array(), new stdClass(), null, 12, false, str_repeat('x', 32769), str_pad('https://example.com', 513), implode("\n", array_fill(0, 101, 'https://example.com'))) as $invalid) { | |
| update_option('es_optimizer_options', array($field => $invalid, $other => 'https://valid.example.com')); | |
| $this->assertSame('https://prior.example.com', get_option('es_optimizer_options')[$field]); | |
| $this->assertSame('https://valid.example.com', get_option('es_optimizer_options')[$other]); | |
| } | |
| } | |
| } | |
| /** | |
| * Raw boundary and canonical origin cases. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_raw_boundary_and_canonical_origin_cases(): void { | |
| foreach (array('https://exa%6dple.com', "https://exa\tmple.com", 'https://example.com\\token', 'https://example.com?synthetic-token', 'https://example.com/path', 'https://127.0.0.1') as $raw) { | |
| $result = es_optimizer_validate_single_domain($raw); | |
| $this->assertFalse($result['valid']); | |
| $this->assertSame('', $result['domain']); | |
| $this->assertStringNotContainsString($raw, $result['error']); | |
| } | |
| $this->assertSame('https://example.com', es_optimizer_validate_single_domain('https://EXAMPLE.COM:443/')['domain']); | |
| $this->assertFalse(es_optimizer_validate_domain_for_context('https://example.com:8443', 'preconnect')['valid']); | |
| $this->assertTrue(es_optimizer_validate_domain_for_context('https://example.com:8443', 'dns_prefetch')['valid']); | |
| $this->assertSame('', es_optimizer_get_domain_list_budget_error(str_pad('https://example.com', 512))); | |
| $this->assertNotSame('', es_optimizer_get_domain_list_budget_error(str_pad('https://example.com', 513))); | |
| $this->assertSame('', es_optimizer_get_domain_list_budget_error(str_repeat(' ', 32768))); | |
| $this->assertNotSame('', es_optimizer_get_domain_list_budget_error(str_repeat(' ', 32769))); | |
| $this->assertSame('', es_optimizer_get_domain_list_budget_error(implode("\r\n", array_fill(0, 100, 'https://example.com')))); | |
| } | |
| /** | |
| * Notice storage dedup and foreign success. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_notice_storage_dedup_and_foreign_success(): void { | |
| add_settings_error('foreign', 'saved', 'Other settings saved.', 'success'); | |
| $input = implode("\n", array_fill(0, 4, 'https://user:synthetic-token@example.com')); | |
| es_optimizer_validate_domain_list($input, 'preconnect'); | |
| es_optimizer_validate_domain_list($input, 'preconnect'); | |
| $warnings = get_settings_errors(); | |
| $this->assertCount(2, $warnings); | |
| set_transient('settings_errors', $warnings, 30); | |
| $this->assertSame($warnings, get_transient('settings_errors')); | |
| $this->assertStringNotContainsString('synthetic-token', serialize(get_transient('settings_errors'))); | |
| $this->assertStringContainsString('4 lines were rejected.', $warnings[1]['message']); | |
| $this->assertStringNotContainsString('Line 4:', $warnings[1]['message']); | |
| ob_start(); | |
| settings_errors(); | |
| $html = ob_get_clean(); | |
| $dom = new DOMDocument(); | |
| $previous = libxml_use_internal_errors(true); | |
| try { | |
| $this->assertTrue($dom->loadHTML($html)); | |
| } finally { | |
| libxml_clear_errors(); | |
| libxml_use_internal_errors($previous); | |
| } | |
| $xpath = new DOMXPath($dom); | |
| $this->assertSame(1, $xpath->query('//*[@id="setting-error-preconnect_security"]')->length); | |
| $this->assertStringContainsString('Other settings saved.', $html); | |
| } | |
| /** | |
| * Settings page capability and native form contract. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_settings_page_capability_and_native_form_contract(): void { | |
| $subscriber = self::factory()->user->create(array('role' => 'subscriber')); | |
| wp_set_current_user($subscriber); | |
| try { | |
| es_optimizer_settings_page(); | |
| $this->fail('Subscriber rendered settings.'); | |
| } catch (WPDieException $error) { | |
| $this->assertStringContainsString('permissions', $error->getMessage()); | |
| } | |
| wp_set_current_user(self::factory()->user->create(array('role' => 'administrator'))); | |
| ob_start(); | |
| es_optimizer_settings_page(); | |
| $html = ob_get_clean(); | |
| $dom = new DOMDocument(); | |
| $previous = libxml_use_internal_errors(true); | |
| try { | |
| $this->assertTrue($dom->loadHTML($html)); | |
| } finally { | |
| libxml_clear_errors(); | |
| libxml_use_internal_errors($previous); | |
| } | |
| $xpath = new DOMXPath($dom); | |
| $this->assertSame(1, $xpath->query('//form[@method="post" and @action="options.php"]')->length); | |
| $this->assertSame('es_optimizer_settings', $xpath->query('//input[@name="option_page"]')->item(0)->getAttribute('value')); | |
| $nonce = $xpath->query('//input[@name="_wpnonce"]')->item(0)->getAttribute('value'); | |
| $this->assertNotFalse(wp_verify_nonce($nonce, 'es_optimizer_settings-options')); | |
| $this->assertFalse(wp_verify_nonce($nonce, 'another-group-options')); | |
| foreach (array('preconnect_domains', 'dns_prefetch_domains') as $field) { | |
| $id = 'es_optimizer_field_' . $field; | |
| $this->assertSame(1, $xpath->query('//textarea[@id="' . $id . '" and @aria-describedby="' . $id . '_description"]')->length); | |
| $this->assertSame(1, $xpath->query('//*[@id="' . $id . '_description"]')->length); | |
| $this->assertSame(1, $xpath->query('//label[@for="' . $id . '"]')->length); | |
| } | |
| } | |
| /** | |
| * Core emitted hint html and legacy preservation. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_core_emitted_hint_html_and_legacy_preservation(): void { | |
| $saved = array_merge(es_optimizer_get_default_options(), array( | |
| 'enable_preconnect' => 1, 'preconnect_domains' => "https://fonts.gstatic.com\nhttps://custom.example.com:8443\nhttps://ok.example.com:443", | |
| 'enable_dns_prefetch' => 1, 'dns_prefetch_domains' => 'https://dns.example.com:8443' | |
| )); | |
| $this->legacy_options($saved); | |
| ob_start(); | |
| wp_resource_hints(); | |
| $html = ob_get_clean(); | |
| $this->assertStringContainsString('https://fonts.gstatic.com', $html); | |
| $this->assertStringContainsString('crossorigin=', $html); | |
| $this->assertStringContainsString('https://ok.example.com', $html); | |
| $this->assertStringContainsString('//dns.example.com', $html); | |
| $this->assertStringNotContainsString('custom.example.com', $html); | |
| $this->assertStringNotContainsString(':8443', $html); | |
| $this->assertSame($saved, get_option('es_optimizer_options')); | |
| } | |
| /** | |
| * Mixed foreign hints and repeated callbacks. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_mixed_foreign_hints_and_repeated_callbacks(): void { | |
| update_option('es_optimizer_options', array('enable_preconnect' => 1, 'preconnect_domains' => 'https://example.com')); | |
| $foreign = array('kept' => array('href' => 'https://example.com', 'crossorigin' => 'use-credentials'), null, 10, array('href' => new stdClass())); | |
| $this->assertSame($foreign, es_optimizer_add_preconnect_resource_hints($foreign, 'preconnect')); | |
| $one = es_optimizer_add_preconnect_resource_hints(array(), 'preconnect'); | |
| $this->assertSame($one, es_optimizer_add_preconnect_resource_hints($one, 'preconnect')); | |
| } | |
| /** | |
| * Oversize legacy read and retired key. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_oversize_legacy_read_and_retired_key(): void { | |
| $saved = array_merge(es_optimizer_get_default_options(), array('enable_preconnect' => 1, 'preconnect_domains' => str_repeat('x', 32769), 'remove_wlw_manifest' => 1)); | |
| $this->legacy_options($saved); | |
| $this->assertSame(array(), es_optimizer_add_preconnect_resource_hints(array(), 'preconnect')); | |
| $this->assertArrayNotHasKey('remove_wlw_manifest', es_optimizer_get_options()); | |
| $this->assertSame($saved, get_option('es_optimizer_options')); | |
| update_option('es_optimizer_options', array('preconnect_domains' => 'https://fixed.example.com')); | |
| $this->assertArrayNotHasKey('remove_wlw_manifest', get_option('es_optimizer_options')); | |
| } | |
| /** | |
| * Native emoji filters off on and admin embed hooks. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_native_emoji_filters_off_on_and_admin_embed_hooks(): void { | |
| $incoming = array('wpemoji', 'foreign'); | |
| update_option('es_optimizer_options', array('disable_emojis' => 0)); | |
| $this->assertSame($incoming, apply_filters('tiny_mce_plugins', $incoming)); | |
| update_option('es_optimizer_options', array('disable_emojis' => 1)); | |
| $this->assertSame(array('foreign'), apply_filters('tiny_mce_plugins', $incoming)); | |
| $hints = array('s.w.org', '//s.w.org', array('href' => 'https://s.w.org/a'), 'keep.example.com', array('href' => null)); | |
| $this->assertSame(array(3 => 'keep.example.com', 4 => array('href' => null)), es_optimizer_disable_emojis_remove_dns_prefetch($hints, 'dns-prefetch')); | |
| add_action('admin_print_scripts', 'print_emoji_detection_script', 10); | |
| add_action('embed_head', 'print_emoji_detection_script', 10); | |
| es_optimizer_disable_admin_emojis(); | |
| es_optimizer_disable_emojis(); | |
| $this->assertFalse(has_action('admin_print_scripts', 'print_emoji_detection_script')); | |
| $this->assertFalse(has_action('embed_head', 'print_emoji_detection_script')); | |
| } | |
| /** | |
| * Jquery header and live external filter contracts. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_jquery_header_and_live_external_filter_contracts(): void { | |
| update_option('es_optimizer_options', array('remove_jquery_migrate' => 1, 'remove_wp_version' => 1, 'remove_rsd_link' => 1, 'remove_shortlink' => 1, 'disable_jetpack_ads' => 0, 'disable_post_via_email' => 0)); | |
| $scripts = new WP_Scripts(); | |
| $scripts->remove('jquery'); | |
| $scripts->add('jquery', false, array('jquery-core', 'jquery-migrate', 'foreign')); | |
| es_optimizer_remove_jquery_migrate($scripts); | |
| $this->assertSame(array('jquery-core', 'foreign'), $scripts->registered['jquery']->deps); | |
| es_optimizer_remove_header_items(); | |
| $this->assertFalse(has_action('wp_head', 'wp_generator')); | |
| $this->assertFalse(has_action('wp_head', 'rsd_link')); | |
| $this->assertFalse(has_action('wp_head', 'wp_shortlink_wp_head')); | |
| $this->assertSame('foreign', apply_filters('jetpack_show_promotions', 'foreign')); | |
| $this->assertSame('foreign', apply_filters('enable_post_by_email_configuration', 'foreign')); | |
| update_option('es_optimizer_options', array('disable_jetpack_ads' => 1, 'disable_post_via_email' => 1)); | |
| $this->assertFalse(apply_filters('jetpack_show_promotions', 'foreign')); | |
| $this->assertFalse(apply_filters('enable_post_by_email_configuration', 'foreign')); | |
| } | |
| /** | |
| * Multisite values and live filters a b a. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_multisite_values_and_live_filters_a_b_a(): void { | |
| $this->require_multisite(); | |
| $a = get_current_blog_id(); | |
| $b = self::factory()->blog->create(); | |
| update_option('es_optimizer_options', array('disable_emojis' => 1, 'preconnect_domains' => 'https://a.example.com')); | |
| switch_to_blog($b); | |
| try { | |
| $this->assertSame(0, $this->option_rows()); | |
| $this->assertSame(es_optimizer_get_default_options(), es_optimizer_get_options()); | |
| update_option('es_optimizer_options', array('disable_emojis' => 0, 'preconnect_domains' => 'https://b.example.com')); | |
| $this->assertSame('https://b.example.com', es_optimizer_get_options()['preconnect_domains']); | |
| $this->assertSame(array('wpemoji', 'foreign'), apply_filters('tiny_mce_plugins', array('wpemoji', 'foreign'))); | |
| } finally { | |
| restore_current_blog(); | |
| } | |
| $this->assertSame($a, get_current_blog_id()); | |
| $this->assertSame('https://a.example.com', es_optimizer_get_options()['preconnect_domains']); | |
| $this->assertSame(array('foreign'), apply_filters('tiny_mce_plugins', array('wpemoji', 'foreign'))); | |
| } | |
| /** | |
| * Uninstall fixed key physical absence and idempotence. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_uninstall_fixed_key_physical_absence_and_idempotence(): void { | |
| update_option('es_optimizer_options', array('preconnect_domains' => 'https://saved.example.com')); | |
| update_option('es_optimizer_foreign_fixture', 'retain'); | |
| es_optimizer_uninstall_current_site_options(); | |
| $this->assertSame(0, $this->option_rows()); | |
| $this->assertSame('retain', get_option('es_optimizer_foreign_fixture')); | |
| es_optimizer_uninstall_current_site_options(); | |
| $this->assertSame(0, $this->option_rows()); | |
| } | |
| /** | |
| * Uninstall stale notoptions cannot prove absence. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_uninstall_stale_notoptions_cannot_prove_absence(): void { | |
| global $wpdb; | |
| update_option('es_optimizer_options', array('preconnect_domains' => 'https://saved.example.com')); | |
| // A hook can restore the physical row after core has marked it absent. | |
| $reinsert = static function () use ($wpdb) { | |
| $wpdb->insert($wpdb->options, array('option_name' => 'es_optimizer_options', 'option_value' => 'fixture', 'autoload' => 'no')); | |
| }; | |
| add_action('delete_option_es_optimizer_options', $reinsert); | |
| try { | |
| es_optimizer_uninstall_current_site_options(); | |
| $this->fail('Physical row survived but cleanup succeeded.'); | |
| } catch (RuntimeException $error) { | |
| $this->assertSame(1, $this->option_rows()); | |
| $this->assertTrue(wp_cache_get('notoptions', 'options')['es_optimizer_options']); | |
| } finally { | |
| remove_action('delete_option_es_optimizer_options', $reinsert); | |
| wp_cache_delete('notoptions', 'options'); | |
| } | |
| es_optimizer_uninstall_current_site_options(); | |
| $this->assertSame(0, $this->option_rows()); | |
| } | |
| /** | |
| * Uninstall failed delete detected and suppression restored. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_uninstall_failed_delete_detected_and_suppression_restored(): void { | |
| global $wpdb; | |
| update_option('es_optimizer_options', array('preconnect_domains' => 'https://saved.example.com')); | |
| $was_suppressed = $wpdb->suppress_errors; | |
| $failure = static function ($query) { | |
| if (str_starts_with($query, 'DELETE FROM') && str_contains($query, 'es_optimizer_options')) { | |
| return 'SELECT * FROM optimizer_intentionally_missing_table'; | |
| } | |
| return $query; | |
| }; | |
| add_filter('query', $failure); | |
| try { | |
| es_optimizer_uninstall_options(); | |
| $this->fail('Failed DELETE was accepted.'); | |
| } catch (RuntimeException $error) { | |
| $this->assertSame(1, $this->option_rows()); | |
| $this->assertSame($was_suppressed, $wpdb->suppress_errors); | |
| $this->assertTrue(wp_cache_get('notoptions', 'options')['es_optimizer_options']); | |
| } finally { | |
| remove_filter('query', $failure); | |
| wp_cache_delete('notoptions', 'options'); | |
| } | |
| es_optimizer_uninstall_options(); | |
| $this->assertSame(0, $this->option_rows()); | |
| } | |
| /** | |
| * Multisite cleanup restores nested context and other site. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_multisite_cleanup_restores_nested_context_and_other_site(): void { | |
| $this->require_multisite(); | |
| $a = get_current_blog_id(); | |
| $b = self::factory()->blog->create(); | |
| update_option('es_optimizer_options', array('preconnect_domains' => 'https://a.example.com')); | |
| switch_to_blog($b); | |
| try { | |
| update_option('es_optimizer_options', array('preconnect_domains' => 'https://b.example.com')); | |
| $stack = $GLOBALS['_wp_switched_stack']; | |
| $switched = $GLOBALS['switched']; | |
| es_optimizer_uninstall_network_options(); | |
| $this->assertSame($b, get_current_blog_id()); | |
| $this->assertSame($stack, $GLOBALS['_wp_switched_stack']); | |
| $this->assertSame($switched, $GLOBALS['switched']); | |
| $this->assertSame(0, $this->option_rows()); | |
| } finally { | |
| restore_current_blog(); | |
| } | |
| $this->assertSame($a, get_current_blog_id()); | |
| $this->assertSame(0, $this->option_rows()); | |
| } | |
| /** | |
| * Multisite cleanup hook failure restores context. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_multisite_cleanup_hook_failure_restores_context(): void { | |
| $this->require_multisite(); | |
| $a = get_current_blog_id(); | |
| $b = self::factory()->blog->create(); | |
| switch_to_blog($b); | |
| update_option('es_optimizer_options', array('preconnect_domains' => 'https://b.example.com')); | |
| restore_current_blog(); | |
| $stack = $GLOBALS['_wp_switched_stack']; | |
| $failure = static function () { throw new RuntimeException('Intentional deletion hook failure.'); }; | |
| add_action('delete_option_es_optimizer_options', $failure); | |
| try { | |
| es_optimizer_uninstall_site_options($b); | |
| $this->fail('Throwing deletion hook was ignored.'); | |
| } catch (RuntimeException $error) { | |
| $this->assertSame('Intentional deletion hook failure.', $error->getMessage()); | |
| } finally { | |
| remove_action('delete_option_es_optimizer_options', $failure); | |
| } | |
| $this->assertSame($a, get_current_blog_id()); | |
| $this->assertSame($stack, $GLOBALS['_wp_switched_stack']); | |
| } | |
| /** | |
| * Site cursor pages are physical and bounded. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_site_cursor_pages_are_physical_and_bounded(): void { | |
| $this->require_multisite(); | |
| global $wpdb; | |
| $last = es_optimizer_uninstall_get_site_limit(); | |
| // Directory-only fixtures test enumeration, not deployment-scale deletion. | |
| for ($i = 1; $i <= 101; ++$i) { | |
| $this->assertNotFalse($wpdb->insert($wpdb->blogs, array( | |
| 'blog_id' => $last + $i, 'site_id' => 1, 'domain' => 'cursor-' . $i . '.example.com', 'path' => '/', | |
| 'registered' => '2026-01-01 00:00:00', 'last_updated' => '2026-01-01 00:00:00' | |
| ))); | |
| } | |
| $maximum = es_optimizer_uninstall_get_site_limit(); | |
| $queries = $wpdb->num_queries; | |
| $page = es_optimizer_uninstall_get_site_ids($last, $maximum); | |
| $this->assertSame(1, $wpdb->num_queries - $queries); | |
| $this->assertCount(100, $page); | |
| $this->assertSame(range($last + 1, $last + 100), $page); | |
| $this->assertSame(array($last + 101), es_optimizer_uninstall_get_site_ids(end($page), $maximum)); | |
| $this->assertSame(array(), es_optimizer_uninstall_get_site_ids($maximum, $maximum)); | |
| } | |
| /** | |
| * Measured hint callback budget. | |
| * | |
| * @since Unreleased | |
| * @return void | |
| */ | |
| public function test_measured_hint_callback_budget(): void { | |
| $measurements = array(); | |
| foreach (array(1, 10, 100) as $candidates) { | |
| $domains = array(); | |
| for ($i = 0; $i < $candidates; ++$i) { | |
| $domains[] = 'https://candidate-' . $i . '.example.com'; | |
| } | |
| update_option('es_optimizer_options', array('enable_preconnect' => 1, 'preconnect_domains' => implode("\n", $domains))); | |
| $medians = array(); | |
| foreach (array(0, 1000, 10000) as $count) { | |
| $samples = array(); | |
| $peak_bytes = 0; | |
| for ($iteration = 0; $iteration < 25; ++$iteration) { | |
| $foreign = array(); | |
| for ($i = 0; $i < $count; ++$i) { | |
| $foreign[] = 'https://foreign-' . $i . '.example.com'; | |
| } | |
| memory_reset_peak_usage(); | |
| $before_memory = memory_get_usage(true); | |
| $start = hrtime(true); | |
| $result = es_optimizer_add_preconnect_resource_hints($foreign, 'preconnect'); | |
| $elapsed = (hrtime(true) - $start) / 1e9; | |
| $extra_peak = memory_get_peak_usage(true) - $before_memory; | |
| $this->assertCount($count + $candidates, $result); | |
| $this->assertSame($foreign, array_slice($result, 0, $count)); | |
| $this->assertLessThanOrEqual(32 * 1024 * 1024, $extra_peak); | |
| if ($iteration >= 5) { | |
| $samples[] = $elapsed; | |
| $peak_bytes = max($peak_bytes, $extra_peak); | |
| } | |
| unset($foreign, $result); | |
| } | |
| sort($samples); | |
| $medians[$count] = ($samples[9] + $samples[10]) / 2; | |
| $measurements[$candidates][$count] = array('median_seconds' => $medians[$count], 'peak_additional_bytes' => $peak_bytes); | |
| } | |
| $this->assertLessThanOrEqual(1.0, $medians[10000]); | |
| $this->assertLessThanOrEqual(max(20 * $medians[1000], 0.1), $medians[10000]); | |
| } | |
| $file = getenv('RUNNER_TEMP') . '/es-optimizer-diagnostics/hint-timing-' . (is_multisite() ? 'multisite' : 'single') . '.json'; | |
| $this->assertNotFalse(file_put_contents($file, wp_json_encode(array('measurements' => $measurements, 'warmups' => 5, 'samples' => 20, 'scope' => 'callback only; not deployment SLA')))); | |
| } | |
| } | |
| EOF | |
| php -l tests/EngineScriptSiteOptimizerTest.php | |
| php -l tests/bootstrap.php | |
| - name: Create PHPUnit Config | |
| id: create_phpunit_config | |
| run: | | |
| cat > phpunit.xml << 'EOF' | |
| <?xml version="1.0"?> | |
| <phpunit bootstrap="tests/bootstrap.php" backupGlobals="false" colors="true" | |
| failOnEmptyTestSuite="true" failOnRisky="true" failOnWarning="true"> | |
| <testsuites> | |
| <testsuite name="EngineScript Site Optimizer native"> | |
| <file>tests/EngineScriptSiteOptimizerTest.php</file> | |
| </testsuite> | |
| </testsuites> | |
| </phpunit> | |
| EOF | |
| - name: Setup WP Tests | |
| id: setup_wp_tests | |
| timeout-minutes: 15 | |
| run: | | |
| bash tests/bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 ${{ matrix.wp-version }} true | |
| - name: Run plugin test | |
| id: run_plugin_test | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| WP_MULTISITE=0 vendor/bin/phpunit --config phpunit.xml --log-junit "$RUNNER_TEMP/es-optimizer-diagnostics/junit.xml" | |
| WP_MULTISITE=1 vendor/bin/phpunit --config phpunit.xml --log-junit "$RUNNER_TEMP/es-optimizer-diagnostics/multisite-junit.xml" | |
| # Prove that the real generated config rejects an empty filtered suite. | |
| if vendor/bin/phpunit --config phpunit.xml --do-not-cache-result \ | |
| --filter '__es_optimizer_intentionally_no_matching_test__' \ | |
| > "$RUNNER_TEMP/es-optimizer-empty-suite.txt" 2>&1; then | |
| echo '::error::An empty test suite incorrectly succeeded.' | |
| exit 1 | |
| else | |
| status=$? | |
| [[ $status == 1 ]] | |
| grep -q 'No tests executed' "$RUNNER_TEMP/es-optimizer-empty-suite.txt" | |
| fi | |
| # This control must fail due to its assertion, not a setup/parse error. | |
| cat > "$RUNNER_TEMP/OptimizerIntentionalFailureTest.php" << 'EOF' | |
| <?php | |
| final class OptimizerIntentionalFailureTest extends PHPUnit\Framework\TestCase { | |
| public function test_intentional_failure(): void { | |
| $this->assertTrue(false, 'Optimizer intentional assertion control'); | |
| } | |
| } | |
| EOF | |
| if vendor/bin/phpunit --no-configuration --do-not-cache-result \ | |
| "$RUNNER_TEMP/OptimizerIntentionalFailureTest.php" \ | |
| > "$RUNNER_TEMP/es-optimizer-failing-suite.txt" 2>&1; then | |
| echo '::error::An intentionally failing assertion incorrectly succeeded.' | |
| exit 1 | |
| else | |
| status=$? | |
| [[ $status == 1 ]] | |
| grep -q 'Optimizer intentional assertion control' "$RUNNER_TEMP/es-optimizer-failing-suite.txt" | |
| fi | |
| echo 'Empty-suite and intentional-assertion controls failed as expected.' | tee "$RUNNER_TEMP/es-optimizer-diagnostics/native-controls.txt" | |
| - name: Preserve diagnostic summary | |
| id: preserve_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| env: | |
| STEP_RESULTS: | | |
| checkout_code=${{ steps.checkout_code.outcome }} | |
| setup_php=${{ steps.setup_php.outcome }} | |
| use_isolated_composer_auth=${{ steps.use_isolated_composer_auth.outcome }} | |
| php_syntax_lint=${{ steps.php_syntax_lint.outcome }} | |
| install_subversion=${{ steps.install_subversion.outcome }} | |
| install_composer_dependencies=${{ steps.install_composer_dependencies.outcome }} | |
| run_isolated_unit_tests=${{ steps.run_isolated_unit_tests.outcome }} | |
| pin_phpunit_for_wordpress_test_suite=${{ steps.pin_phpunit_for_wordpress_test_suite.outcome }} | |
| verify_phpunit_dependency_versions=${{ steps.verify_phpunit_dependency_versions.outcome }} | |
| prepare_database=${{ steps.prepare_database.outcome }} | |
| create_tests_directory_structure=${{ steps.create_tests_directory_structure.outcome }} | |
| create_wp_tests_install_script=${{ steps.create_wp_tests_install_script.outcome }} | |
| create_bootstrap_file=${{ steps.create_bootstrap_file.outcome }} | |
| create_test_file=${{ steps.create_test_file.outcome }} | |
| create_phpunit_config=${{ steps.create_phpunit_config.outcome }} | |
| setup_wp_tests=${{ steps.setup_wp_tests.outcome }} | |
| run_plugin_test=${{ steps.run_plugin_test.outcome }} | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| printf '%s\n' "$STEP_RESULTS" > "$RUNNER_TEMP/es-optimizer-diagnostics/steps.txt" | |
| printf 'commit=%s\njob=%s\n' "$GITHUB_SHA" "$GITHUB_JOB" > "$RUNNER_TEMP/es-optimizer-diagnostics/run.txt" | |
| if command -v php >/dev/null; then | |
| php -r 'echo PHP_VERSION, PHP_EOL;' > "$RUNNER_TEMP/es-optimizer-diagnostics/php.txt" | |
| fi | |
| if command -v composer >/dev/null && [ -f vendor/composer/installed.json ]; then | |
| php -r '$data = json_decode(file_get_contents("vendor/composer/installed.json"), true, 512, JSON_THROW_ON_ERROR); foreach ($data["packages"] ?? $data as $package) { echo $package["name"], " ", $package["version"], PHP_EOL; }' > "$RUNNER_TEMP/es-optimizer-diagnostics/dependencies.txt" | |
| fi | |
| # Never upload config, authentication data, SQL, exports, or arbitrary logs. | |
| # Refuse oversized or linked diagnostics instead of uploading them. | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type l -delete | |
| find "$RUNNER_TEMP/es-optimizer-diagnostics" -type f -size +2M -delete | |
| - name: Upload diagnostic summary | |
| id: upload_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: wp-compat-${{ matrix.php-version }}-${{ matrix.wp-version }}-${{ matrix.dependency-versions }} | |
| path: | | |
| ${{ runner.temp }}/es-optimizer-diagnostics/run.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/steps.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/php.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/dependencies.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/wordpress.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/junit.xml | |
| ${{ runner.temp }}/es-optimizer-diagnostics/unit-junit.xml | |
| ${{ runner.temp }}/es-optimizer-diagnostics/multisite-junit.xml | |
| ${{ runner.temp }}/es-optimizer-diagnostics/unit-dependencies.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/unit-controls.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/native-controls.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/hint-timing-single.json | |
| ${{ runner.temp }}/es-optimizer-diagnostics/hint-timing-multisite.json | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| - name: Report test status | |
| id: report_test_status | |
| if: ${{ always() }} | |
| run: | | |
| if [ ${{ job.status }} == 'success' ]; then | |
| echo "✅ Tests passed successfully on PHP ${{ matrix.php-version }} with WordPress ${{ matrix.wp-version }}" | |
| else | |
| echo "❌ Tests failed on PHP ${{ matrix.php-version }} with WordPress ${{ matrix.wp-version }}" | |
| fi | |
| - name: Ensure repository files available after failure | |
| id: ensure_repository_files_available_after_failure | |
| if: ${{ failure() }} | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Create issue on test failure | |
| id: create_issue_on_test_failure | |
| if: ${{ failure() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' }} | |
| uses: JasonEtco/create-an-issue@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| FAILURE_STAGE: ${{ steps.checkout_code.outcome == 'failure' && 'checkout_code' || steps.setup_php.outcome == 'failure' && 'setup_php' || steps.use_isolated_composer_auth.outcome == 'failure' && 'use_isolated_composer_auth' || steps.php_syntax_lint.outcome == 'failure' && 'php_syntax_lint' || steps.install_subversion.outcome == 'failure' && 'install_subversion' || steps.install_composer_dependencies.outcome == 'failure' && 'install_composer_dependencies' || steps.run_isolated_unit_tests.outcome == 'failure' && 'run_isolated_unit_tests' || steps.pin_phpunit_for_wordpress_test_suite.outcome == 'failure' && 'pin_phpunit_for_wordpress_test_suite' || steps.verify_phpunit_dependency_versions.outcome == 'failure' && 'verify_phpunit_dependency_versions' || steps.prepare_database.outcome == 'failure' && 'prepare_database' || steps.create_tests_directory_structure.outcome == 'failure' && 'create_tests_directory_structure' || steps.create_wp_tests_install_script.outcome == 'failure' && 'create_wp_tests_install_script' || steps.create_bootstrap_file.outcome == 'failure' && 'create_bootstrap_file' || steps.create_test_file.outcome == 'failure' && 'create_test_file' || steps.create_phpunit_config.outcome == 'failure' && 'create_phpunit_config' || steps.setup_wp_tests.outcome == 'failure' && 'setup_wp_tests' || steps.run_plugin_test.outcome == 'failure' && 'run_plugin_test' || steps.report_test_status.outcome == 'failure' && 'report_test_status' || steps.ensure_repository_files_available_after_failure.outcome == 'failure' && 'ensure_repository_files_available_after_failure' || 'unknown_or_canceled' }} | |
| PHP_VERSION: ${{ matrix.php-version }} | |
| WP_VERSION: ${{ matrix.wp-version }} | |
| DEPENDENCY_VERSIONS: ${{ matrix.dependency-versions }} | |
| RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| filename: .github/ISSUE_TEMPLATE/wp-version-test-failure.md | |
| update_existing: false |