Skip to content

Workflow fixes

Workflow fixes #217

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