Skip to content

Instantly share code, notes, and snippets.

@masonjames
Created August 15, 2025 20:32
Show Gist options
  • Select an option

  • Save masonjames/0e02dfed138e03c114d225a213a329d1 to your computer and use it in GitHub Desktop.

Select an option

Save masonjames/0e02dfed138e03c114d225a213a329d1 to your computer and use it in GitHub Desktop.
#!/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