Created
August 15, 2025 20:32
-
-
Save masonjames/0e02dfed138e03c114d225a213a329d1 to your computer and use it in GitHub Desktop.
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
| #!/usr/bin/env bash | |
| # Unified EA Plugins - Local CI Test Runner | |
| # Runs consistent checks across any EA plugin repository. | |
| # Usage: bash ./run-tests.sh [--fix] [--skip-js] [--skip-php] [--skip-security] | |
| # Exit codes: 0 = all pass (or skipped), 1 = one or more failures. | |
| set -Eeuo pipefail | |
| # ------------ Colors ------------ | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| NC='\033[0m' # no color | |
| # ------------ Options ------------ | |
| FIX=0 | |
| SKIP_JS=0 | |
| SKIP_PHP=0 | |
| SKIP_SECURITY=0 | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --fix) FIX=1; shift ;; | |
| --skip-js) SKIP_JS=1; shift ;; | |
| --skip-php) SKIP_PHP=1; shift ;; | |
| --skip-security) SKIP_SECURITY=1; shift ;; | |
| *) echo -e "${YELLOW}Unknown option: $1${NC}"; shift ;; | |
| esac | |
| done | |
| # ------------ Helpers ------------ | |
| has_cmd() { command -v "$1" >/dev/null 2>&1; } | |
| section() { echo -e "${BLUE}\n== $1 ==${NC}"; } | |
| ok() { echo -e "${GREEN}✓ $1${NC}"; } | |
| warn() { echo -e "${YELLOW}⚠ $1${NC}"; } | |
| err() { echo -e "${RED}✗ $1${NC}"; } | |
| ROOT_DIR="$(pwd)" | |
| # Exclude globs used by several steps | |
| FIND_EXCLUDES=(-not -path "*/vendor/*" -not -path "*/node_modules/*" -not -path "*/assets/dist/*" -not -path "*/admin/assets/dist/*" -not -path "*/build/*" -not -path "*/dist/*") | |
| find_plugin_main() { | |
| # Prefer PHP files at repo root which contain a WP plugin header | |
| local main | |
| main="$(find "${ROOT_DIR}" -maxdepth 1 -type f -name "*.php" | while read -r f; do | |
| if grep -q "Plugin Name:" "$f"; then echo "$f"; break; fi | |
| done || true)" | |
| if [[ -z "${main}" ]]; then | |
| # Broader search (excluding vendor/node_modules/etc) | |
| main="$(grep -R --include="*.php" -n "Plugin Name:" . \ | |
| --exclude-dir vendor --exclude-dir node_modules --exclude-dir build --exclude-dir dist \ | |
| --exclude-dir assets/dist --exclude-dir admin/assets/dist 2>/dev/null | head -n1 | cut -d: -f1 || true)" | |
| fi | |
| echo "${main:-}" | |
| } | |
| PLUGIN_MAIN="$(find_plugin_main)" | |
| echo "=======================================" | |
| echo "EA Plugins - Local CI Test Runner" | |
| echo "Repository: $(basename "${ROOT_DIR}")" | |
| echo "Plugin main file: ${PLUGIN_MAIN:-not found}" | |
| echo "Options -> fix:${FIX} skip-js:${SKIP_JS} skip-php:${SKIP_PHP} skip-security:${SKIP_SECURITY}" | |
| echo "=======================================" | |
| TOTAL_FAIL=0 | |
| # ------------ Composer sanity (optional) ------------ | |
| if has_cmd composer && [[ -f composer.json ]]; then | |
| section "Composer Validate" | |
| if composer validate --strict --no-interaction >/dev/null 2>&1; then | |
| ok "composer.json is valid" | |
| else | |
| warn "composer.json validation reported issues (not failing build)" | |
| fi | |
| fi | |
| # ------------ 1) PHP Syntax ------------ | |
| if [[ "${SKIP_PHP}" -eq 0 ]]; then | |
| section "PHP Syntax Check" | |
| if ! has_cmd php; then | |
| warn "PHP not found; skipping PHP checks" | |
| else | |
| set +e | |
| PHP_LINT_OUTPUT="$(find . -type f -name "*.php" "${FIND_EXCLUDES[@]}" -print0 | xargs -0 -n 1 -P 4 php -l 2>&1)" | |
| PHP_LINT_RC=$? | |
| set -e | |
| if [[ ${PHP_LINT_RC} -eq 0 ]] && ! echo "${PHP_LINT_OUTPUT}" | grep -E "Parse error|Fatal error" >/dev/null; then | |
| ok "PHP syntax: PASSED" | |
| else | |
| err "PHP syntax: FAILED" | |
| echo "${PHP_LINT_OUTPUT}" | grep -E "Parse error|Fatal error" || true | |
| TOTAL_FAIL=$((TOTAL_FAIL+1)) | |
| fi | |
| fi | |
| else | |
| warn "PHP checks skipped by option" | |
| fi | |
| # ------------ 2) PHPCS (WPCS) ------------ | |
| if [[ "${SKIP_PHP}" -eq 0 ]]; then | |
| section "WordPress Coding Standards (PHPCS)" | |
| PHPCS_RAN=0 | |
| PHPCS_RC=0 | |
| if has_cmd composer && composer run --list --no-interaction 2>/dev/null | grep -q " phpcs "; then | |
| set +e | |
| composer run -q phpcs | |
| PHPCS_RC=$? | |
| set -e | |
| PHPCS_RAN=1 | |
| elif [[ -x "./vendor/bin/phpcs" ]]; then | |
| set +e | |
| ./vendor/bin/phpcs | |
| PHPCS_RC=$? | |
| set -e | |
| PHPCS_RAN=1 | |
| elif has_cmd phpcs; then | |
| set +e | |
| phpcs | |
| PHPCS_RC=$? | |
| set -e | |
| PHPCS_RAN=1 | |
| fi | |
| if [[ "${PHPCS_RAN}" -eq 1 ]]; then | |
| if [[ "${PHPCS_RC}" -eq 0 ]]; then | |
| ok "PHPCS: PASSED" | |
| else | |
| err "PHPCS: FAILED" | |
| [[ "${FIX}" -eq 1 ]] && { echo "Running PHPCBF to attempt fixes..."; (composer run -q phpcbf || ./vendor/bin/phpcbf || phpcbf) || true; } | |
| TOTAL_FAIL=$((TOTAL_FAIL+1)) | |
| fi | |
| else | |
| warn "PHPCS not available. Install dev dependencies to enable (composer install)." | |
| fi | |
| fi | |
| # ------------ 3) PHPCompatibility ------------ | |
| if [[ "${SKIP_PHP}" -eq 0 ]]; then | |
| section "PHP Compatibility (PHPCompatibilityWP)" | |
| PHPCOMP_RAN=0 | |
| PHPCOMP_RC=0 | |
| if has_cmd composer && composer run --list --no-interaction 2>/dev/null | grep -q " php-check "; then | |
| set +e | |
| composer run -q php-check | |
| PHPCOMP_RC=$? | |
| set -e | |
| PHPCOMP_RAN=1 | |
| elif [[ -x "./vendor/bin/phpcs" ]]; then | |
| set +e | |
| ./vendor/bin/phpcs . --standard=PHPCompatibilityWP --severity=1 --runtime-set testVersion 7.4- --extensions=php,inc --ignore=vendor,node_modules,assets/dist,admin/assets/dist,build,dist | |
| PHPCOMP_RC=$? | |
| set -e | |
| PHPCOMP_RAN=1 | |
| fi | |
| if [[ "${PHPCOMP_RAN}" -eq 1 ]]; then | |
| if [[ "${PHPCOMP_RC}" -eq 0 ]]; then | |
| ok "PHPCompatibility: PASSED" | |
| else | |
| err "PHPCompatibility: FAILED" | |
| TOTAL_FAIL=$((TOTAL_FAIL+1)) | |
| fi | |
| else | |
| warn "PHPCompatibility sniff not available. Ensure dev tools installed." | |
| fi | |
| fi | |
| # ------------ 4) ESLint ------------ | |
| if [[ "${SKIP_JS}" -eq 0 ]]; then | |
| section "JavaScript/TypeScript Lint (ESLint)" | |
| if [[ -f package.json ]]; then | |
| if ! has_cmd node || ! has_cmd npm; then | |
| warn "Node.js/npm not found; skipping ESLint" | |
| else | |
| if [[ ! -d node_modules ]]; then | |
| echo "node_modules not found; attempting install..." | |
| if npm --version >/dev/null 2>&1; then | |
| (npm ci --prefer-offline --no-audit --fund=false || npm install --no-audit --fund=false) >/dev/null 2>&1 || true | |
| fi | |
| fi | |
| set +e | |
| npm run lint --silent --if-present | |
| ESLINT_RC=$? | |
| if [[ ${ESLINT_RC} -ne 0 ]]; then | |
| npx -y eslint . 2>/dev/null | |
| ESLINT_RC=$? | |
| fi | |
| set -e | |
| if [[ ${ESLINT_RC} -eq 0 ]]; then | |
| ok "ESLint: PASSED" | |
| else | |
| err "ESLint: FAILED" | |
| [[ "${FIX}" -eq 1 ]] && { echo "Running ESLint autofix..."; npm run lint:fix --silent --if-present || npx -y eslint . --fix || true; } | |
| TOTAL_FAIL=$((TOTAL_FAIL+1)) | |
| fi | |
| # Optional TypeScript compile check if tsconfig exists | |
| if [[ -f tsconfig.json ]] || [[ -f assets/games/tsconfig.json ]]; then | |
| section "TypeScript Compilation (noEmit)" | |
| set +e | |
| if has_cmd npx; then | |
| npx -y tsc --noEmit --pretty false 2>/dev/null | |
| TSC_RC=$? | |
| else | |
| TSC_RC=0 | |
| fi | |
| set -e | |
| if [[ ${TSC_RC} -eq 0 ]]; then | |
| ok "TypeScript: PASSED" | |
| else | |
| err "TypeScript: FAILED" | |
| TOTAL_FAIL=$((TOTAL_FAIL+1)) | |
| fi | |
| fi | |
| fi | |
| else | |
| warn "No package.json found; skipping JS lint" | |
| fi | |
| else | |
| warn "JS checks skipped by option" | |
| fi | |
| # ------------ 5) Plugin Header Validation ------------ | |
| section "Plugin Header Validation" | |
| if [[ -n "${PLUGIN_MAIN}" && -f "${PLUGIN_MAIN}" ]]; then | |
| if grep -q "Plugin Name:" "${PLUGIN_MAIN}"; then | |
| ok "Plugin header present in ${PLUGIN_MAIN}" | |
| else | |
| err "Plugin header missing in ${PLUGIN_MAIN}" | |
| TOTAL_FAIL=$((TOTAL_FAIL+1)) | |
| fi | |
| else | |
| warn "Main plugin file not found; skipping header validation" | |
| fi | |
| # ------------ 6) Basic Security Scan ------------ | |
| if [[ "${SKIP_SECURITY}" -eq 0 ]]; then | |
| section "Basic Security Scan (heuristics)" | |
| SECURITY_WARN=0 | |
| # eval() | |
| if grep -R --line-number --include="*.php" -E "\beval\s*\(" . --exclude-dir vendor --exclude-dir node_modules 2>/dev/null | grep -v "// phpcs:ignore" | grep -q .; then | |
| warn "Potential eval() usage detected" | |
| SECURITY_WARN=$((SECURITY_WARN+1)) | |
| fi | |
| # $wpdb calls without prepare() | |
| if grep -R --line-number --include="*.php" -E "\$wpdb->(query|get_var|get_row|get_results)\s*\(" . --exclude-dir vendor --exclude-dir node_modules 2>/dev/null \ | |
| | grep -v "prepare\s*\(" | grep -v "// phpcs:ignore" | grep -q .; then | |
| warn "Potential unprepared \$wpdb calls detected" | |
| SECURITY_WARN=$((SECURITY_WARN+1)) | |
| fi | |
| # Unsanitized superglobals (rough heuristic) | |
| if grep -R --line-number --include="*.php" -E "\$_(GET|POST|REQUEST|COOKIE)\s*\[" . --exclude-dir vendor --exclude-dir node_modules 2>/dev/null \ | |
| | grep -v -E "sanitize_|esc_|wp_verify_nonce|check_admin_referer|filter_input" \ | |
| | grep -v "// phpcs:ignore" | head -5 | grep -q .; then | |
| warn "Potentially unsanitized input usage" | |
| SECURITY_WARN=$((SECURITY_WARN+1)) | |
| fi | |
| # Direct file ops (encourage WP_Filesystem) | |
| if grep -R --line-number --include="*.php" -E "\b(file_put_contents|fopen|fwrite|unlink|rename|copy|chmod|mkdir|rmdir)\s*\(" . --exclude-dir vendor --exclude-dir node_modules 2>/dev/null \ | |
| | grep -v "WP_Filesystem" | grep -v "// phpcs:ignore" | grep -q .; then | |
| warn "Direct filesystem calls detected (consider WP_Filesystem)" | |
| SECURITY_WARN=$((SECURITY_WARN+1)) | |
| fi | |
| # unserialize / base64_decode red flags | |
| if grep -R --line-number --include="*.php" -E "\bunserialize\s*\(|\bbase64_decode\s*\(" . --exclude-dir vendor --exclude-dir node_modules 2>/dev/null \ | |
| | grep -v "// phpcs:ignore" | grep -q .; then | |
| warn "Usage of unserialize()/base64_decode() detected; confirm safe usage" | |
| SECURITY_WARN=$((SECURITY_WARN+1)) | |
| fi | |
| # ABSPATH guard in main plugin file | |
| if [[ -n "${PLUGIN_MAIN}" && -f "${PLUGIN_MAIN}" ]]; then | |
| if ! grep -q -E "defined\s*\(\s*'ABSPATH'\s*\)\s*(\|\||or)\s*(exit|die)\s*;" "${PLUGIN_MAIN}" 2>/dev/null; then | |
| warn "Main plugin file may be missing ABSPATH guard" | |
| SECURITY_WARN=$((SECURITY_WARN+1)) | |
| fi | |
| fi | |
| if [[ ${SECURITY_WARN} -eq 0 ]]; then | |
| ok "Security scan: no findings" | |
| else | |
| warn "Security scan: ${SECURITY_WARN} warning(s)" | |
| fi | |
| else | |
| warn "Security scan skipped by option" | |
| fi | |
| # ------------ Summary ------------ | |
| echo -e "${BLUE}\n=======================================" | |
| echo "SUMMARY" | |
| echo "=======================================${NC}" | |
| if [[ ${TOTAL_FAIL} -eq 0 ]]; then | |
| ok "All checks PASSED" | |
| exit 0 | |
| else | |
| err "${TOTAL_FAIL} check(s) FAILED" | |
| echo "" | |
| echo "Helpful commands:" | |
| echo " composer run phpcbf # Auto-fix PHPCS issues" | |
| echo " composer run phpcs # View PHPCS issues" | |
| echo " composer run php-check # PHPCompatibility details" | |
| echo " npm run lint # View ESLint issues (if configured)" | |
| echo "Re-run with '--fix' to attempt autofixes where possible." | |
| exit 1 | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment