Repository navigation
Remove renovate.json, unused: Dependabot updates the dependencies #245
Workflow file for this run
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.3, 8.4, 8.5) | |
| # - PHP compatibility testing across multiple PHP versions (8.3, 8.4, 8.5) | |
| # - WordPress compatibility testing across multiple WP versions (7.0, latest, nightly) | |
| # - PHPStan static analysis for WordPress-specific code quality | |
| # - Rector dry run, which fails on PHP that a current idiom would replace | |
| # - WordPress security vulnerability scanning using pattern analysis | |
| # - PHPCS code standards validation for WordPress coding standards | |
| # - Psalm static analysis and taint analysis | |
| # - YAML, workflow, Python and Markdown linting | |
| # (yamllint, actionlint, zizmor, Ruff, mypy, markdownlint) | |
| # - Unit test line coverage, with a minimum | |
| # - 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 | |
| jobs: | |
| plugin-check: | |
| name: WP Plugin Check (PHP 8.3) | |
| runs-on: ubuntu-latest | |
| # The job files an issue when it fails on main. | |
| permissions: | |
| contents: read | |
| issues: write | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # 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@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 | |
| 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/ || 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 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@10857da14b6c2246d15402b3e69f777edcf8c12e # 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 | |
| # Run the experimental checks as well | |
| include-experimental: true | |
| # Don't ignore warnings or errors, including the low-severity ones | |
| ignore-warnings: false | |
| ignore-errors: false | |
| include-low-severity-errors: true | |
| include-low-severity-warnings: true | |
| # A warning fails the job, as an error does | |
| strict: true | |
| # WordPress version to use | |
| wp-version: 'latest' | |
| - name: Handle plugin check completion | |
| id: handle_plugin_check_completion | |
| if: ${{ always() }} | |
| env: | |
| JOB_STATUS: ${{ job.status }} | |
| 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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| 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@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 | |
| 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 | |
| # The job files an issue when it fails on main. | |
| permissions: | |
| contents: read | |
| issues: write | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Setup PHP 8.3 | |
| id: setup_php_8_3 | |
| uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 | |
| 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@26d8a556604053a9612623447203a691f406fbe6 # 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 array_find(array(), "is_int");\n' > "$CONTROL_DIR/modern-incompatible.php" | |
| vendor/bin/phpcs --standard=PHPCompatibilityWP --runtime-set testVersion 8.3 "$CONTROL_DIR/compatible.php" | |
| if vendor/bin/phpcs --standard=PHPCompatibilityWP --runtime-set testVersion 8.3 --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.3 --report=json "$CONTROL_DIR/modern-incompatible.php" > "$CONTROL_DIR/modern.json"; then | |
| echo '::warning::CR-TOOL-001: current stable rules miss PHP 8.4 array_find at the PHP 8.3 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"], "array_find")) { 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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| 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@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 | |
| 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 | |
| # The job files an issue when it fails on main. | |
| permissions: | |
| contents: read | |
| issues: write | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Setup PHP 8.3 | |
| id: setup_php_8_3 | |
| uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 | |
| 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@26d8a556604053a9612623447203a691f406fbe6 # 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"/> | |
| <!-- Report every message, including those rated below the default severity of 5 --> | |
| <arg name="severity" value="1"/> | |
| </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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| 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@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 | |
| 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 | |
| # The job files an issue when it fails on main. | |
| permissions: | |
| contents: read | |
| issues: write | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # 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@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 | |
| 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/ || 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 analyze . --no-progress --format text --ruleset phpmd.xml --exclude vendor --exclude tests --exclude stubs --exclude .private --exclude node_modules | |
| - 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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| 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@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 | |
| 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 | |
| # The job files an issue when it fails on main. | |
| permissions: | |
| contents: read | |
| issues: write | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Setup PHP 8.3 | |
| id: setup_php_8_3 | |
| uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 | |
| 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@26d8a556604053a9612623447203a691f406fbe6 # 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: Run Psalm taint analysis | |
| id: run_psalm_taint_analysis | |
| run: | | |
| vendor/bin/psalm --config=psalm.xml --taint-analysis | |
| - 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_psalm_taint_analysis=${{ steps.run_psalm_taint_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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| 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@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 | |
| 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' || steps.run_psalm_taint_analysis.outcome == 'failure' && 'run_psalm_taint_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/psalm-failure.md | |
| update_existing: false | |
| security-check: | |
| name: Security Scan (PHP 8.3) | |
| runs-on: ubuntu-latest | |
| # The job files an issue when it fails on main. | |
| permissions: | |
| contents: read | |
| issues: write | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Setup PHP 8.3 | |
| id: setup_php_8_3 | |
| uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 | |
| 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@26d8a556604053a9612623447203a691f406fbe6 # 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@258311ef7ac571f1310780ef3d79fc5abef642b5 # 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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| 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@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 | |
| 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 | |
| # The job files an issue when it fails on main. | |
| permissions: | |
| contents: read | |
| issues: write | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # 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@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 | |
| 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/ || 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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| 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@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 | |
| 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 | |
| rector: | |
| name: Rector (PHP 8.3) | |
| runs-on: ubuntu-latest | |
| # The job files an issue when it fails on main. | |
| permissions: | |
| contents: read | |
| issues: write | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # 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@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 | |
| 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/ || true | |
| - name: Install Composer dependencies (no cache) | |
| id: install_composer_dependencies_no_cache | |
| run: | | |
| composer install --prefer-dist --no-progress --no-cache | |
| - name: Run Rector dry run | |
| id: run_rector_dry_run | |
| # The dry run changes nothing and fails when any configured rule would change the code. | |
| run: | | |
| vendor/bin/rector --version | |
| vendor/bin/rector process --dry-run --no-progress-bar | |
| - 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 }} | |
| run_rector_dry_run=${{ steps.run_rector_dry_run.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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| with: | |
| name: wp-compat-rector | |
| 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 | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| - name: Create issue on Rector failure | |
| id: create_issue_on_rector_failure | |
| if: ${{ failure() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' }} | |
| uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 | |
| 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.run_rector_dry_run.outcome == 'failure' && 'run_rector_dry_run' || '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/rector-failure.md | |
| update_existing: false | |
| lint: | |
| name: Lint (Node 24) | |
| runs-on: ubuntu-latest | |
| # The job files an issue when it fails on main. | |
| permissions: | |
| contents: read | |
| issues: write | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Node.js 24 | |
| id: setup_node | |
| uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 | |
| with: | |
| node-version: '24' | |
| - name: Install npm dependencies | |
| id: install_npm_dependencies | |
| run: | | |
| npm ci --no-audit --no-fund | |
| npm ls markdownlint-cli2 | |
| - name: Install Python linters | |
| id: install_python_linters | |
| run: | | |
| # A virtual environment holds the versions that .github/lint-requirements.txt pins. | |
| python3 -m venv "$RUNNER_TEMP/lint-venv" | |
| "$RUNNER_TEMP/lint-venv/bin/pip" install --disable-pip-version-check --quiet \ | |
| --requirement .github/lint-requirements.txt | |
| echo "$RUNNER_TEMP/lint-venv/bin" >> "$GITHUB_PATH" | |
| - name: Run yamllint | |
| id: run_yamllint | |
| run: | | |
| # Lint the tracked files only, so installed dependencies are left out. | |
| mapfile -d '' -t YAML_FILES < <(git ls-files -z -- '*.yml' '*.yaml') | |
| # A run that found no files must not pass. | |
| [[ ${#YAML_FILES[@]} -gt 0 ]] | |
| # --strict makes a warning fail the job, as an error does. | |
| yamllint --strict -c .yamllint.yml -- "${YAML_FILES[@]}" | |
| - name: Run actionlint | |
| id: run_actionlint | |
| run: | | |
| # ShellCheck and pyflakes are left out. With ShellCheck, actionlint can stall on the | |
| # long steps of the compatibility workflow; Ruff and mypy check the Python scripts. | |
| actionlint -shellcheck= -pyflakes= .github/workflows/*.yml | |
| - name: Run zizmor | |
| id: run_zizmor | |
| # --offline: the audits that need the GitHub API are left out, so no token is used. | |
| run: zizmor --offline --no-progress .github/workflows | |
| - name: Run Ruff | |
| id: run_ruff | |
| run: | | |
| ruff check .github/scripts | |
| ruff format --check .github/scripts | |
| - name: Run mypy | |
| id: run_mypy | |
| # .mypy.ini names the files and sets the strict mode. | |
| run: mypy | |
| - name: Run markdownlint | |
| id: run_markdownlint | |
| run: | | |
| # .markdownlint-cli2.jsonc names the files; .markdownlint.json holds the rules. | |
| status=0 | |
| npm run --silent lint:md > "$RUNNER_TEMP/markdownlint.txt" 2>&1 || status=$? | |
| cat "$RUNNER_TEMP/markdownlint.txt" | |
| [[ $status == 0 ]] | |
| # A run that found no files must not pass. | |
| grep -Eq '^Linting: [1-9][0-9]* file' "$RUNNER_TEMP/markdownlint.txt" | |
| - name: Preserve diagnostic summary | |
| id: preserve_diagnostics | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| env: | |
| STEP_RESULTS: | | |
| checkout_code=${{ steps.checkout_code.outcome }} | |
| setup_node=${{ steps.setup_node.outcome }} | |
| install_npm_dependencies=${{ steps.install_npm_dependencies.outcome }} | |
| install_python_linters=${{ steps.install_python_linters.outcome }} | |
| run_yamllint=${{ steps.run_yamllint.outcome }} | |
| run_actionlint=${{ steps.run_actionlint.outcome }} | |
| run_zizmor=${{ steps.run_zizmor.outcome }} | |
| run_ruff=${{ steps.run_ruff.outcome }} | |
| run_mypy=${{ steps.run_mypy.outcome }} | |
| run_markdownlint=${{ steps.run_markdownlint.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 node >/dev/null; then | |
| node --version > "$RUNNER_TEMP/es-optimizer-diagnostics/node.txt" | |
| fi | |
| if command -v npm >/dev/null && [ -d node_modules ]; then | |
| npm ls --all --depth=1 > "$RUNNER_TEMP/es-optimizer-diagnostics/dependencies.txt" 2>&1 || true | |
| fi | |
| if [ -x "$RUNNER_TEMP/lint-venv/bin/pip" ]; then | |
| "$RUNNER_TEMP/lint-venv/bin/pip" freeze > "$RUNNER_TEMP/es-optimizer-diagnostics/python-dependencies.txt" 2>&1 || true | |
| 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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| with: | |
| name: wp-compat-lint | |
| path: | | |
| ${{ runner.temp }}/es-optimizer-diagnostics/run.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/steps.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/node.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/dependencies.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/python-dependencies.txt | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| - name: Create issue on lint failure | |
| id: create_issue_on_lint_failure | |
| if: ${{ failure() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' }} | |
| uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| FAILURE_STAGE: ${{ steps.checkout_code.outcome == 'failure' && 'checkout_code' || steps.setup_node.outcome == 'failure' && 'setup_node' || steps.install_npm_dependencies.outcome == 'failure' && 'install_npm_dependencies' || steps.install_python_linters.outcome == 'failure' && 'install_python_linters' || steps.run_yamllint.outcome == 'failure' && 'run_yamllint' || steps.run_actionlint.outcome == 'failure' && 'run_actionlint' || steps.run_zizmor.outcome == 'failure' && 'run_zizmor' || steps.run_ruff.outcome == 'failure' && 'run_ruff' || steps.run_mypy.outcome == 'failure' && 'run_mypy' || steps.run_markdownlint.outcome == 'failure' && 'run_markdownlint' || 'unknown_or_canceled' }} | |
| NODE_VERSION: '24' | |
| RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| filename: .github/ISSUE_TEMPLATE/lint-failure.md | |
| update_existing: false | |
| unit-coverage: | |
| name: Unit Test Coverage (PHP 8.4) | |
| runs-on: ubuntu-latest | |
| # The job files an issue when it fails on main. | |
| permissions: | |
| contents: read | |
| issues: write | |
| timeout-minutes: 15 | |
| env: | |
| # The unit suite must cover at least this percentage of the production lines. It is set a few | |
| # points below the measured value, so it stops a real drop without failing on small changes. | |
| MINIMUM_LINE_COVERAGE: '72' | |
| steps: | |
| - name: Checkout code | |
| id: checkout_code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Setup PHP 8.4 | |
| id: setup_php_8_4 | |
| uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 | |
| with: | |
| php-version: '8.4' | |
| extensions: mysqli, curl, zip, intl, gd, mbstring, fileinfo, xml | |
| coverage: pcov | |
| tools: composer:v2 | |
| - name: Install Composer dependencies | |
| id: install_composer_dependencies | |
| uses: ramsey/composer-install@26d8a556604053a9612623447203a691f406fbe6 # v4 | |
| with: | |
| dependency-versions: highest | |
| composer-options: "--prefer-dist --no-progress" | |
| - name: Run unit tests with coverage | |
| id: run_unit_tests_with_coverage | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/es-optimizer-diagnostics" | |
| vendor/bin/phpunit --version | |
| vendor/bin/phpunit --configuration phpunit.xml --testsuite unit --fail-on-empty-test-suite \ | |
| --coverage-text="$RUNNER_TEMP/es-optimizer-diagnostics/coverage.txt" \ | |
| --coverage-clover "$RUNNER_TEMP/es-optimizer-diagnostics/clover.xml" | |
| cat "$RUNNER_TEMP/es-optimizer-diagnostics/coverage.txt" | |
| - name: Check line coverage | |
| id: check_line_coverage | |
| run: | | |
| php -r ' | |
| $metrics = simplexml_load_file($argv[1])->project->metrics; | |
| $lines = (int) $metrics["statements"]; | |
| $covered = (int) $metrics["coveredstatements"]; | |
| if ($lines < 1) { | |
| fwrite(STDERR, "::error::The coverage report lists no production lines.\n"); | |
| exit(1); | |
| } | |
| $percent = 100 * $covered / $lines; | |
| printf("Line coverage: %.2f%% (%d of %d lines); minimum %s%%.\n", $percent, $covered, $lines, $argv[2]); | |
| if ($percent < (float) $argv[2]) { | |
| fwrite(STDERR, "::error::Unit test line coverage is below the minimum.\n"); | |
| exit(1); | |
| } | |
| ' "$RUNNER_TEMP/es-optimizer-diagnostics/clover.xml" "$MINIMUM_LINE_COVERAGE" | |
| - 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_4=${{ steps.setup_php_8_4.outcome }} | |
| install_composer_dependencies=${{ steps.install_composer_dependencies.outcome }} | |
| run_unit_tests_with_coverage=${{ steps.run_unit_tests_with_coverage.outcome }} | |
| check_line_coverage=${{ steps.check_line_coverage.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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| with: | |
| name: wp-compat-unit-coverage | |
| 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/coverage.txt | |
| ${{ runner.temp }}/es-optimizer-diagnostics/clover.xml | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| - name: Create issue on unit coverage failure | |
| id: create_issue_on_unit_coverage_failure | |
| if: ${{ failure() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' }} | |
| uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| FAILURE_STAGE: ${{ steps.checkout_code.outcome == 'failure' && 'checkout_code' || steps.setup_php_8_4.outcome == 'failure' && 'setup_php_8_4' || steps.install_composer_dependencies.outcome == 'failure' && 'install_composer_dependencies' || steps.run_unit_tests_with_coverage.outcome == 'failure' && 'run_unit_tests_with_coverage' || steps.check_line_coverage.outcome == 'failure' && 'check_line_coverage' || 'unknown_or_canceled' }} | |
| PHP_VERSION: '8.4' | |
| RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| filename: .github/ISSUE_TEMPLATE/unit-coverage-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 | |
| # The job files an issue when it fails on main. | |
| permissions: | |
| contents: read | |
| issues: write | |
| timeout-minutes: 45 | |
| strategy: | |
| matrix: | |
| php-version: ['8.3', '8.4', '8.5'] | |
| wp-version: ['7.0', 'latest', 'nightly'] | |
| dependency-versions: ['highest'] | |
| include: | |
| - php-version: '8.3' | |
| 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Setup PHP ${{ matrix.php-version }} | |
| id: setup_php | |
| uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 | |
| 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: Resolve dependencies for this PHP version | |
| id: resolve_dependencies_for_this_php_version | |
| run: | | |
| # composer.json resolves composer.lock for the PHP 8.3 floor (config.platform); | |
| # this job updates the dependencies, so it resolves them for the PHP it runs. | |
| composer config platform.php "$(php -r 'echo PHP_VERSION;')" | |
| - name: Install Composer dependencies | |
| id: install_composer_dependencies | |
| uses: ramsey/composer-install@26d8a556604053a9612623447203a691f406fbe6 # 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'; | |
| EOF | |
| - name: Create Test File | |
| id: create_test_file | |
| run: | | |
| # The generated suite lives in tests/integration so that the WordPress | |
| # configuration below does not pick up the tracked tests/unit suite. | |
| mkdir -p tests/integration | |
| cat > tests/integration/EngineScriptSiteOptimizerTest.php << 'EOF' | |
| <?php | |
| /** Native WordPress regression cases; generated and run only on fresh CI runners. */ | |
| final class EngineScriptSiteOptimizerTest extends WP_UnitTestCase { | |
| /** | |
| * Load uninstall helpers after PHPUnit publishes WordPress globals. | |
| * | |
| * @since 2.2.0 | |
| * @return void | |
| */ | |
| public static function set_up_before_class() { | |
| parent::set_up_before_class(); | |
| // PHPUnit 9.6 exports bootstrap variables only after the file returns. | |
| // The exact entry runs once on the empty disposable installation. | |
| // Helper tests cover DB/cache/failure semantics, not core file deletion. | |
| define('WP_UNINSTALL_PLUGIN', 'enginescript-site-optimizer/enginescript-site-optimizer.php'); | |
| require_once dirname(__DIR__, 2) . '/uninstall.php'; | |
| } | |
| /** | |
| * Reset native state for each regression. | |
| * | |
| * @since 2.2.0 | |
| * @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 2.2.0 | |
| * @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 2.2.0 | |
| * @return void | |
| */ | |
| public function test_native_bootstrap_mode_and_optimizer_hooks(): void { | |
| $this->assertSame('1' === getenv('WP_MULTISITE'), is_multisite()); | |
| $this->assertTrue(function_exists('es_optimizer_uninstall_options')); | |
| if (is_multisite()) { | |
| $this->assertSame(array(), $GLOBALS['_wp_switched_stack']); | |
| $this->assertFalse($GLOBALS['switched']); | |
| } | |
| $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 2.2.0 | |
| * @return void | |
| */ | |
| public function test_virtual_defaults_activation_and_deactivation(): void { | |
| // Activation and deactivation have no callbacks: settings stay virtual until a save and survive deactivation. | |
| $basename = plugin_basename(ES_SITE_OPTIMIZER_FILE); | |
| $this->assertFalse(has_action('activate_' . $basename)); | |
| $this->assertFalse(has_action('deactivate_' . $basename)); | |
| $this->assertSame(es_optimizer_get_default_options(), es_optimizer_get_options()); | |
| $this->assertSame('', es_optimizer_get_options()['preconnect_domains']); | |
| $this->assertSame('', es_optimizer_get_options()['dns_prefetch_domains']); | |
| $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://fonts.googleapis.com', | |
| 'dns_prefetch_domains' => 'https://adservice.google.com' | |
| )); | |
| update_option('es_optimizer_options', $saved); | |
| $this->assertSame($saved, get_option('es_optimizer_options')); | |
| $this->assertSame(1, $this->option_rows()); | |
| } | |
| /** | |
| * Native add update delete without manual refresh. | |
| * | |
| * @since 2.2.0 | |
| * @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 2.2.0 | |
| * @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 2.2.0 | |
| * @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 2.2.0 | |
| * @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 2.2.0 | |
| * @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 2.2.0 | |
| * @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 2.2.0 | |
| * @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')); | |
| $suggestions = array( | |
| 'preconnect_domains' => "https://fonts.googleapis.com\nhttps://fonts.gstatic.com\nhttps://s.w.org\nhttps://wordpress.com\nhttps://cdnjs.cloudflare.com\nhttps://www.googletagmanager.com", | |
| 'dns_prefetch_domains' => 'https://adservice.google.com' | |
| ); | |
| foreach ($suggestions as $field => $suggested_domains) { | |
| $id = 'es_optimizer_field_' . $field; | |
| $textarea = $xpath->query('//textarea[@id="' . $id . '" and @aria-describedby="' . $id . '_description"]'); | |
| $this->assertSame(1, $textarea->length); | |
| $this->assertSame('', $textarea->item(0)->textContent); | |
| $description = $xpath->query('//textarea[@id="' . $id . '"]/following-sibling::div[1][@id="' . $id . '_description"]'); | |
| $this->assertSame(1, $description->length); | |
| $this->assertStringContainsString('Optional suggestions:', $description->item(0)->textContent); | |
| $examples = $xpath->query('.//pre', $description->item(0)); | |
| $this->assertSame(1, $examples->length); | |
| $this->assertSame($suggested_domains, $examples->item(0)->textContent); | |
| $this->assertSame(1, $xpath->query('//label[@for="' . $id . '"]')->length); | |
| } | |
| $this->assertSame(0, $this->option_rows()); | |
| $saved = array_merge(es_optimizer_get_default_options(), array( | |
| 'preconnect_domains' => 'https://saved.example.com', | |
| 'dns_prefetch_domains' => 'https://dns.example.com' | |
| )); | |
| update_option('es_optimizer_options', $saved); | |
| foreach (array('preconnect_domains', 'dns_prefetch_domains') as $field) { | |
| ob_start(); | |
| es_optimizer_render_textarea_field(array('label_for' => 'es_optimizer_field_' . $field)); | |
| $field_html = ob_get_clean(); | |
| $this->assertTrue($dom->loadHTML($field_html)); | |
| $this->assertSame($saved[$field], $dom->getElementsByTagName('textarea')->item(0)->textContent); | |
| } | |
| $this->assertSame($saved, get_option('es_optimizer_options')); | |
| } | |
| /** | |
| * Core emitted hint html and legacy preservation. | |
| * | |
| * @since 2.2.0 | |
| * @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 2.2.0 | |
| * @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 2.2.0 | |
| * @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 2.2.0 | |
| * @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)); | |
| // Core prints no emoji DNS hint since WordPress 6.8; the plugin no longer filters one. | |
| $this->assertFalse(has_filter('wp_resource_hints', 'es_optimizer_disable_emojis_remove_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 2.2.0 | |
| * @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(PHP_INT_MAX, has_filter('enable_post_by_email_configuration', 'es_optimizer_filter_post_via_email')); | |
| $this->assertSame('foreign', es_optimizer_filter_post_via_email('foreign')); | |
| $this->assertFalse(es_optimizer_filter_post_via_email(false)); | |
| // Core disables post-by-email on multisite unless POST_BY_EMAIL opts in. | |
| $mail_enabled = !is_multisite() || (defined('POST_BY_EMAIL') && POST_BY_EMAIL); | |
| $this->assertSame($mail_enabled, apply_filters('enable_post_by_email_configuration', true)); | |
| 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(es_optimizer_filter_post_via_email(true)); | |
| $this->assertFalse(apply_filters('enable_post_by_email_configuration', true)); | |
| } | |
| /** | |
| * Multisite values and live filters a b a. | |
| * | |
| * @since 2.2.0 | |
| * @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 2.2.0 | |
| * @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 2.2.0 | |
| * @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 2.2.0 | |
| * @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); | |
| if (is_multisite()) { | |
| // The network uninstall clears the runtime cache after each site (CR-ARCH-002), core's notoptions mark included. | |
| $this->assertFalse(wp_cache_get('notoptions', 'options')); | |
| } else { | |
| $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 2.2.0 | |
| * @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 2.2.0 | |
| * @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 2.2.0 | |
| * @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 2.2.0 | |
| * @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')))); | |
| } | |
| /** | |
| * Network uninstall with the switch globals unset, as WP-CLI leaves them (CR-API-001). | |
| * | |
| * @since 2.3.0 | |
| * @return void | |
| */ | |
| public function test_network_uninstall_with_switch_globals_unset(): void { | |
| $this->require_multisite(); | |
| $sites = $this->sites_with_options(2); | |
| $stack = $GLOBALS['_wp_switched_stack']; | |
| $switched = $GLOBALS['switched']; | |
| unset($GLOBALS['_wp_switched_stack'], $GLOBALS['switched']); | |
| try { | |
| es_optimizer_uninstall_network_options(); | |
| $this->assertSame($sites[0], get_current_blog_id()); | |
| } finally { | |
| $GLOBALS['_wp_switched_stack'] = $stack; | |
| $GLOBALS['switched'] = $switched; | |
| } | |
| $this->assertSame(array_fill(0, 3, 0), $this->rows_per_site($sites)); | |
| } | |
| /** | |
| * The network uninstall clears the in-memory cache after each site, so memory does not grow with the network (CR-ARCH-002). | |
| * | |
| * @since 2.3.0 | |
| * @return void | |
| */ | |
| public function test_network_uninstall_clears_the_runtime_cache(): void { | |
| $this->require_multisite(); | |
| $sites = $this->sites_with_options(2); | |
| $this->assertTrue(wp_cache_supports('flush_runtime')); | |
| wp_cache_set('probe', 'kept', 'es_optimizer_test'); | |
| es_optimizer_uninstall_network_options(); | |
| $this->assertFalse(wp_cache_get('probe', 'es_optimizer_test')); | |
| $this->assertSame($sites[0], get_current_blog_id()); | |
| $this->assertSame(array_fill(0, 3, 0), $this->rows_per_site($sites)); | |
| } | |
| /** | |
| * A site whose cleanup cannot be verified is passed over and named; later sites are still cleaned (CR-ARCH-001, CR-SEM-004). | |
| * | |
| * @since 2.3.0 | |
| * @return void | |
| */ | |
| public function test_network_uninstall_passes_over_a_site_it_cannot_verify(): void { | |
| $this->require_multisite(); | |
| global $wpdb; | |
| $sites = $this->sites_with_options(2); | |
| $table = $wpdb->get_blog_prefix($sites[1]) . 'options'; | |
| $failure = static function ($query) use ($table) { | |
| if (str_starts_with($query, 'SELECT option_id FROM') && str_contains($query, $table)) { | |
| return 'SELECT * FROM optimizer_intentionally_missing_table'; | |
| } | |
| return $query; | |
| }; | |
| add_filter('query', $failure); | |
| try { | |
| es_optimizer_uninstall_options(); | |
| $this->fail('A site whose cleanup could not be verified was accepted.'); | |
| } catch (RuntimeException $error) { | |
| $this->assertSame(ES_SITE_OPTIMIZER_UNINSTALL_ERROR, $error->getCode()); | |
| $this->assertStringContainsString('Settings could not be removed from 1 site. Site ID: ' . $sites[1] . '.', $error->getMessage()); | |
| $this->assertStringContainsString('The settings of site ' . $sites[1] . ' could not be verified as removed.', $error->getMessage()); | |
| } finally { | |
| remove_filter('query', $failure); | |
| } | |
| $this->assertSame($sites[0], get_current_blog_id()); | |
| $this->assertSame(array_fill(0, 3, 0), $this->rows_per_site($sites), 'The site after the failing one was cleaned too.'); | |
| } | |
| /** | |
| * A failure that leaves another site in place stops the routine at once (CR-ARCH-001). | |
| * | |
| * @since 2.3.0 | |
| * @return void | |
| */ | |
| public function test_network_uninstall_stops_when_the_context_cannot_be_restored(): void { | |
| $this->require_multisite(); | |
| $sites = $this->sites_with_options(2); | |
| switch_to_blog($sites[2]); | |
| // Pops the routine's own frame and the caller's. | |
| $steal = static function () { | |
| restore_current_blog(); | |
| restore_current_blog(); | |
| }; | |
| add_action('delete_option_es_optimizer_options', $steal); | |
| try { | |
| es_optimizer_uninstall_network_options(); | |
| $this->fail('A broken context was not noticed.'); | |
| } catch (RuntimeException $error) { | |
| $this->assertSame('WordPress could not switch back to the original site.', $error->getMessage()); | |
| } finally { | |
| remove_action('delete_option_es_optimizer_options', $steal); | |
| while (ms_is_switched()) { | |
| restore_current_blog(); | |
| } | |
| } | |
| $this->assertSame(array(0, 1, 1), $this->rows_per_site($sites), 'Nothing after the first site was touched.'); | |
| } | |
| /** | |
| * Only the uninstall's own failures show a reason, and the list of sites is capped (CR-SEM-004). | |
| * | |
| * @since 2.3.0 | |
| * @return void | |
| */ | |
| public function test_uninstall_failure_messages(): void { | |
| $own = es_optimizer_uninstall_get_failure_message(new RuntimeException('The list of network sites could not be read.', ES_SITE_OPTIMIZER_UNINSTALL_ERROR)); | |
| $this->assertSame('Site Optimizer settings could not be fully removed. The plugin files were retained. Pause site and settings changes, then retry removal. Reason: The list of network sites could not be read.', $own); | |
| $foreign = es_optimizer_uninstall_get_failure_message(new RuntimeException('secret detail')); | |
| $this->assertStringNotContainsString('secret', $foreign); | |
| $this->assertStringNotContainsString('Reason:', $foreign); | |
| $capped = es_optimizer_uninstall_get_sites_failure_reason(250, range(1, 20), new Exception('foreign')); | |
| $this->assertSame('Settings could not be removed from 250 sites. Site IDs, at most the first 20: ' . implode(', ', range(1, 20)) . '.', $capped); | |
| } | |
| /** | |
| * The header options act on the head, the response header, and the feeds; an export keeps its generator (CR-API-002, D-012). | |
| * | |
| * @since 2.3.0 | |
| * @return void | |
| */ | |
| public function test_header_cleanup_reaches_feeds_but_not_exports(): void { | |
| update_option('es_optimizer_options', array('remove_wp_version' => 1, 'remove_shortlink' => 1)); | |
| es_optimizer_remove_header_items(); | |
| $this->assertFalse(has_action('wp_head', 'wp_generator')); | |
| $this->assertFalse(has_action('template_redirect', 'wp_shortlink_header')); | |
| foreach (array('commentsrss2_head', 'rss_head', 'rdf_header', 'atom_head', 'comments_atom_head', 'opml_head', 'app_head') as $action) { | |
| $this->assertFalse(has_action($action, 'the_generator'), $action); | |
| } | |
| // An export runs this action inside the file it writes; outside a feed request it keeps its tag. | |
| $this->assertSame(10, has_action('rss2_head', 'the_generator')); | |
| es_optimizer_remove_feed_generator(); | |
| ob_start(); | |
| do_action('rss2_head'); | |
| $this->assertStringContainsString('<generator>https://wordpress.org/?v=', (string) ob_get_clean()); | |
| $GLOBALS['wp_query']->is_feed = true; | |
| try { | |
| es_optimizer_remove_feed_generator(); | |
| } finally { | |
| $GLOBALS['wp_query']->is_feed = false; | |
| } | |
| $this->assertFalse(has_action('rss2_head', 'the_generator')); | |
| } | |
| /** | |
| * The login screen's head gets no hint from the plugin; the front end still does (Q-005). | |
| * | |
| * @since 2.3.0 | |
| * @return void | |
| */ | |
| public function test_login_screen_gets_no_plugin_hints(): void { | |
| update_option('es_optimizer_options', array('enable_preconnect' => 1, 'preconnect_domains' => 'https://cdn.example.com', 'enable_dns_prefetch' => 1, 'dns_prefetch_domains' => 'https://dns.example.com')); | |
| remove_all_actions('login_head'); | |
| $seen = array(); | |
| add_action('login_head', static function () use (&$seen) { | |
| $seen = array_merge(apply_filters('wp_resource_hints', array(), 'preconnect'), apply_filters('wp_resource_hints', array(), 'dns-prefetch')); | |
| }); | |
| do_action('login_head'); | |
| $this->assertStringNotContainsString('example.com', (string) wp_json_encode($seen)); | |
| $front = wp_json_encode(array_merge(apply_filters('wp_resource_hints', array(), 'preconnect'), apply_filters('wp_resource_hints', array(), 'dns-prefetch'))); | |
| $this->assertStringContainsString('cdn.example.com', (string) $front); | |
| $this->assertStringContainsString('dns.example.com', (string) $front); | |
| } | |
| /** | |
| * A save with a rejected line is confirmed under the native setting; the plugin's own notices stay one (CR-UI-001). | |
| * | |
| * @since 2.3.0 | |
| * @return void | |
| */ | |
| public function test_save_with_a_rejected_line_is_confirmed(): void { | |
| es_optimizer_validate_options(array('preconnect_domains' => "https://ok.example.com\nnot a url")); | |
| $this->assertCount(1, get_settings_errors('es_optimizer_options')); | |
| $general = get_settings_errors('general'); | |
| $this->assertCount(1, $general); | |
| $this->assertSame(array('settings_updated', 'success', 'Settings saved.'), array($general[0]['code'], $general[0]['type'], $general[0]['message'])); | |
| } | |
| /** | |
| * A hostile translation cannot put markup into the admin menu, which WordPress prints unescaped (CR-SEC-001). | |
| * | |
| * @since 2.3.0 | |
| * @return void | |
| */ | |
| public function test_menu_titles_are_escaped_under_a_hostile_translation(): void { | |
| wp_set_current_user(self::factory()->user->create(array('role' => 'administrator'))); | |
| $evil = static function ($translation, $text, $domain) { | |
| return 'enginescript-site-optimizer' === $domain ? '<img src=x onerror=alert(1)>' . $text : $translation; | |
| }; | |
| $backup = $GLOBALS['submenu'] ?? null; | |
| $GLOBALS['submenu'] = array(); | |
| add_filter('gettext', $evil, 10, 3); | |
| try { | |
| es_optimizer_add_settings_page(); | |
| $entry = $GLOBALS['submenu']['options-general.php'][0]; | |
| } finally { | |
| remove_filter('gettext', $evil, 10); | |
| $GLOBALS['submenu'] = $backup; | |
| } | |
| $this->assertSame('<img src=x onerror=alert(1)>Site Optimizer', $entry[0]); | |
| } | |
| /** | |
| * Text that is not an origin is rejected with WordPress's own URL functions (CR-SEM-005). | |
| * | |
| * @since 2.3.0 | |
| * @return void | |
| */ | |
| public function test_text_that_is_not_an_origin_is_rejected(): void { | |
| foreach (array('https://cdn.example.org:8$443', 'https://example.org:443.5', 'https://example.org: 443', 'https://example.org:', 'https://[example.org]') as $input) { | |
| $this->assertFalse(es_optimizer_validate_single_domain($input)['valid'], $input); | |
| } | |
| $this->assertSame('', es_optimizer_validate_options(array('preconnect_domains' => 'https://cdn.example.org:8$443'))['preconnect_domains']); | |
| $this->assertSame('https://example.com', es_optimizer_validate_single_domain('https://EXAMPLE.COM:443/')['domain']); | |
| } | |
| /** | |
| * A stored "false" is off (CR-SEM-003), and a null from other code passes the plugin's filters (CR-SEM-002). | |
| * | |
| * @since 2.3.0 | |
| * @return void | |
| */ | |
| public function test_stored_false_and_a_foreign_null(): void { | |
| $this->legacy_options(array('disable_emojis' => 'false')); | |
| $this->assertFalse(es_optimizer_is_option_enabled('disable_emojis')); | |
| es_optimizer_disable_emojis(); | |
| $this->assertSame(7, has_action('wp_head', 'print_emoji_detection_script')); | |
| add_filter('wp_resource_hints', '__return_null', 5); | |
| try { | |
| $this->assertNull(apply_filters('wp_resource_hints', array(), 'preconnect')); | |
| $this->assertNull(apply_filters('wp_resource_hints', array(), 'dns-prefetch')); | |
| } finally { | |
| remove_filter('wp_resource_hints', '__return_null', 5); | |
| } | |
| $this->assertNull(apply_filters('tiny_mce_plugins', null)); | |
| } | |
| /** | |
| * Emoji style removal depends on core's back-compatibility check, which this pins (O-012). | |
| * | |
| * @since 2.3.0 | |
| * @return void | |
| */ | |
| public function test_emoji_styles_follow_core_back_compatibility(): void { | |
| update_option('es_optimizer_options', array('disable_emojis' => 1)); | |
| es_optimizer_disable_emojis(); | |
| wp_enqueue_emoji_styles(); | |
| $this->assertFalse(wp_style_is('wp-emoji-styles', 'enqueued')); | |
| add_action('wp_print_styles', 'print_emoji_styles'); | |
| wp_enqueue_emoji_styles(); | |
| $this->assertTrue(wp_style_is('wp-emoji-styles', 'enqueued'), 'Control: with the legacy callback in place core enqueues the style.'); | |
| wp_dequeue_style('wp-emoji-styles'); | |
| } | |
| /** | |
| * Create sites and give each site, the current one first, a stored option. | |
| * | |
| * @param int $count Number of sites to create. | |
| * @return array<int, int> Site IDs, the current site first. | |
| */ | |
| private function sites_with_options(int $count): array { | |
| $sites = array(get_current_blog_id()); | |
| for ($i = 0; $i < $count; ++$i) { | |
| $sites[] = self::factory()->blog->create(); | |
| } | |
| foreach ($sites as $site) { | |
| switch_to_blog($site); | |
| update_option('es_optimizer_options', array('disable_emojis' => 1)); | |
| restore_current_blog(); | |
| } | |
| return $sites; | |
| } | |
| /** | |
| * The physical option rows of each site. | |
| * | |
| * @param array<int, int> $sites Site IDs. | |
| * @return array<int, int> Row count per site, in the same order. | |
| */ | |
| private function rows_per_site(array $sites): array { | |
| $rows = array(); | |
| foreach ($sites as $site) { | |
| switch_to_blog($site); | |
| $rows[] = $this->option_rows(); | |
| restore_current_blog(); | |
| } | |
| return $rows; | |
| } | |
| } | |
| EOF | |
| php -l tests/integration/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"> | |
| <directory suffix="Test.php">./tests/integration/</directory> | |
| </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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| 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() }} | |
| env: | |
| JOB_STATUS: ${{ job.status }} | |
| MATRIX_PHP_VERSION: ${{ matrix.php-version }} | |
| MATRIX_WP_VERSION: ${{ matrix.wp-version }} | |
| 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| 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@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 | |
| 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 |