diff --git a/.env.example b/.env.example index 9f37843..4829dc8 100644 --- a/.env.example +++ b/.env.example @@ -5,19 +5,48 @@ # cp .env.example .env # ============================================================================= -# Project -# COMPOSE_PROJECT_NAME prefixes all container, volume and network names. -# Change this to run multiple independent test environments from the same repo. -# Example: COMPOSE_PROJECT_NAME=wptesting-elementify +# Project Kennung (outside-visible identifier) +# ───────────────────────────────────────────────────────────────────────────── +# COMPOSE_PROJECT_NAME prefixes all container, volume and network names — +# it's the "kennung" by which `docker ps` shows this stack apart from any +# other wp-test-env-derived stack on the same machine. Each overlay MUST +# pick a unique value (kebab-case, suffix `-wptest` recommended). +# +# Examples: +# COMPOSE_PROJECT_NAME=wptesting # default / first instance +# COMPOSE_PROJECT_NAME=elementeer-wptest # elementeer overlay +# COMPOSE_PROJECT_NAME=capacium-wptest # capacium-bridge overlay +# +# See docs/overlay-pattern.md for the full coexistence pattern. COMPOSE_PROJECT_NAME=wptesting # Project Directory # When set, the environment stores its state (volumes, uploads, logs) in a -# subdirectory under envs// instead of the repo root. This keeps -# projects isolated and the repo clean. -# Example: COMPOSE_PROJECT_DIR=envs/elementify +# subdirectory under envs// instead of the repo root. Keeps +# overlays isolated even when re-using the same wp-test-env checkout. +# Example: COMPOSE_PROJECT_DIR=envs/capacium-bridge-tests COMPOSE_PROJECT_DIR= +# Per-instance bind-mount paths (CRITICAL for multi-instance isolation) +# ───────────────────────────────────────────────────────────────────────────── +# By default the docker-compose binds ./plugins, ./themes, ./uploads from the +# repo root. If two overlays share the same wp-test-env checkout, they'd see +# each other's plugins — defeating isolation. Override per-instance: +# +# PLUGINS_DIR=./envs/capacium-bridge-tests/plugins +# THEMES_DIR=./envs/capacium-bridge-tests/themes +# UPLOADS_DIR=./envs/capacium-bridge-tests/uploads +# +# IMPORTANT: each path MUST be prefixed with ./ — docker-compose treats +# unprefixed values as named-volume references, not bind mounts, and the +# compose file fails with "refers to undefined volume". +# +# setup.sh creates these dirs on first run and seeds plugins/ with the wp-test-env +# canonical bundle (Query Monitor, Debug Bar, User Switching, WP Importer). +PLUGINS_DIR=./plugins +THEMES_DIR=./themes +UPLOADS_DIR=./uploads + # MySQL MYSQL_ROOT_PASSWORD=wordpress_root_password MYSQL_DATABASE=wordpress_test diff --git a/.gitignore b/.gitignore index 2b78e01..eac77c4 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,8 @@ coverage/ # Project environments (isolated state per project) envs/ + +# Plugins live only in overlays — template never ships them. README.md +# is allowed as a placeholder so the dir exists in fresh clones. +plugins/* +!plugins/README.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8bb9561 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,212 @@ +# wp-test-env — Agents Guide + +**Audience:** any AI coding agent (Claude Code, Cursor, OpenCode, etc.) and any human dev who lands here looking for "how do I run WordPress tests for my plugin/theme." + +**TL;DR:** This is a **template**. It is not the place where your project lives. You consume it from your own project repository as an overlay. Never fork this repo to add project-specific plugins/themes/configs. + +--- + +## ⚠ Before you touch anything + +If your task is "run WordPress tests for plugin/theme X": + +1. **Do NOT add plugin X's files, configs, or test scripts to this repo.** This template is OSS and must stay generic. Project-specific artifacts belong in the project's own overlay (see "Overlay layout" below). +2. **Do NOT modify** `docker-compose.yml`, `scripts/setup.sh`, `config/plugins.yaml`, or `.env.example` to make your project work. If you need to override behavior, do it via env vars in your overlay's `.env`. +3. **Do NOT commit a `.env`** with your project's secrets to this repo. `.env` is gitignored. +4. **Do clone this repo once** somewhere stable (e.g. `~/Documents/repositories/github/wp-test-env` or `~/wp-test-env`). Your project's overlay setup script will find it via env var `WP_TEST_ENV_ROOT` or auto-detection. + +If your task is "extend wp-test-env's generic capabilities" (e.g. add a new database engine option, fix a docker-compose bug, add a new test helper that EVERY project would benefit from): yes, edit this repo directly. + +--- + +## How this template is meant to be used + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Template (this repo) — OSS, generic, shared by all projects │ +│ /Users/andrelange/Documents/repositories/github/wp-test-env │ +│ │ +│ • docker-compose.yml (parametrized via env vars) │ +│ • scripts/setup.sh (calls check-ports.sh, creates dirs) │ +│ • scripts/check-ports.sh (port-collision pre-flight) │ +│ • docs/overlay-pattern.md (the convention) │ +│ • .env.example (defaults + comments) │ +│ • plugins/README.md (placeholder — plugins/* gitignored) │ +└─────────────────────────────────────────────────────────────────────┘ + ▲ + │ consumes (does NOT fork) + ┌─────────────────┴─────────────────────────┐ + │ │ +┌─────────────┴───────────────┐ ┌─────────────────┴─────────────────┐ +│ Overlay (project-specific) │ │ Overlay (project-specific) │ +│ capacium-bridge-tests/ │ │ elementeer-ops/wp-testing-env/ │ +│ │ │ │ +│ • .env.overlay (kennung) │ │ • config/elementeer.env (kennung) │ +│ • config/plugins.yaml │ │ • config/plugins.yaml │ +│ • scripts/setup-overlay.sh │ │ • scripts/setup-elementeer.sh │ +│ • bundles/*.zip │ │ • bundles/*.zip │ +│ • tests/*.sh │ │ • tests/*.sh │ +│ • AGENTS.md / README.md │ │ • AGENTS.md / README.md │ +└─────────────────────────────┘ └────────────────────────────────────┘ +``` + +The overlay holds everything project-specific: +- Plugin under test (or symlink/cp into the isolated PLUGINS_DIR at setup time) +- Premium plugin ZIPs (in `bundles/`) +- The kennung (`COMPOSE_PROJECT_NAME`), port band, and per-instance bind-mount paths +- Project-specific smoke tests +- Project-specific plugin profile (`plugins.yaml`) + +The template provides: +- Generic Docker stack (MySQL + WordPress + WP-CLI + phpMyAdmin + MailHog) +- Generic plugin installer (reads `config/plugins.yaml`, installs from wordpress.org / URL / local zips) +- Port-collision pre-flight (`check-ports.sh`) +- Per-instance state isolation (`COMPOSE_PROJECT_NAME` + `COMPOSE_PROJECT_DIR` + `PLUGINS_DIR`/`THEMES_DIR`/`UPLOADS_DIR`) + +--- + +## Overlay layout (what your project repo should look like) + +``` +-tests/ ← your overlay repo +├── README.md +├── AGENTS.md ← project-specific agent rules +├── wp-testing-env/ +│ ├── .env.overlay ← THE manifest: kennung + ports + paths +│ ├── config/ +│ │ └── plugins.yaml ← your plugin profile +│ ├── bundles/ ← premium ZIPs (gitignored) +│ ├── scripts/ +│ │ └── setup-overlay.sh ← your overlay's setup orchestrator +│ └── tests/ ← project-specific smoke tests +``` + +Reference implementations: +- **Capacium:** https://github.com/Capacium/capacium-bridge-tests +- **Elementeer:** `/Users/andrelange/Documents/repositories/forgejo/elementeer/elementeer-ops/wp-testing-env` + +--- + +## Minimum `.env.overlay` your overlay must declare + +```bash +# Kennung — unique per overlay +COMPOSE_PROJECT_NAME=myproject-wptest +COMPOSE_PROJECT_DIR=envs/myproject-tests + +# Port band — must not collide with other overlays +WORDPRESS_PORT=8084 # check pre-allocated bands in wp-test-env docs/overlay-pattern.md +MYSQL_PORT=3309 +PHPMYADMIN_PORT=8085 +MAILHOG_SMTP_PORT=1027 +MAILHOG_WEB_PORT=8027 + +# Per-instance bind-mount paths (MUST be prefixed with ./ — see env.example) +PLUGINS_DIR=./envs/myproject-tests/plugins +THEMES_DIR=./envs/myproject-tests/themes +UPLOADS_DIR=./envs/myproject-tests/uploads + +# Plugin profile from your overlay +PLUGINS_CONFIG=config/plugins.yaml + +# DB creds — project-specific so they don't clash +MYSQL_DATABASE=myproject_test +MYSQL_USER=myproject +MYSQL_PASSWORD=myproject_test_password +WORDPRESS_TABLE_PREFIX=mp_ +``` + +Pre-allocated port bands per overlay live in `docs/overlay-pattern.md`. Pick the next free band. + +--- + +## Minimum `setup-overlay.sh` shape + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# 1. Locate wp-test-env (env or auto-detect via sibling dirs) +WP_TEST_ENV_ROOT="${WP_TEST_ENV_ROOT:-$HOME/Documents/repositories/github/wp-test-env}" +[ -d "$WP_TEST_ENV_ROOT" ] || { echo "wp-test-env not found"; exit 1; } + +# 2. Verify base supports multi-instance (v2.1+) +[ -x "$WP_TEST_ENV_ROOT/scripts/check-ports.sh" ] || { + echo "wp-test-env base too old — needs v2.1+ (PR LangeVC/wp-test-env#7)" + exit 1 +} + +# 3. Apply overlay manifest: stash existing, copy ours +[ -f "$WP_TEST_ENV_ROOT/.env" ] && \ + cp "$WP_TEST_ENV_ROOT/.env" "$WP_TEST_ENV_ROOT/.env.pre-myproject-$(date +%s)" +cp "$(dirname "$0")/../.env.overlay" "$WP_TEST_ENV_ROOT/.env" +cp "$(dirname "$0")/../config/plugins.yaml" "$WP_TEST_ENV_ROOT/config/plugins.yaml" + +# 4. Run base setup (port-checks + docker compose up + plugin install) +(cd "$WP_TEST_ENV_ROOT" && ./scripts/setup.sh) + +# 5. Drop your project's main plugin into the now-isolated PLUGINS_DIR +# (NOT via docker cp into a shared container — write to the bind mount on host) +cp -r "$MY_PROJECT_SOURCE" "$WP_TEST_ENV_ROOT/envs/myproject-tests/plugins/my-plugin" + +# 6. Activate via wp-cli + run smoke tests +docker exec myproject-wptest-wordpress wp plugin activate my-plugin --allow-root +bash "$(dirname "$0")/../tests/smoke-test.sh" +``` + +(See capacium-bridge-tests/wp-testing-env/scripts/setup-overlay.sh for the full reference.) + +--- + +## Common pitfalls + +| Pitfall | Why it bites | Fix | +|---------|--------------|-----| +| `docker exec wptesting-wordpress …` | "wptesting" is just the default — whatever overlay is on default ports owns it. Your dev machine probably has one already. | Use your own kennung's container name: `docker exec myproject-wptest-wordpress …` | +| `docker cp my-plugin INTO_CONTAINER:/var/www/html/wp-content/plugins/` | Writes to the BIND MOUNT, which (without `PLUGINS_DIR` override) is the SHARED `./plugins` dir → pollutes other overlays | Set `PLUGINS_DIR=./envs//plugins` and `cp` onto the host bind mount, not via `docker cp` into the shared default | +| `git add plugins/my-plugin/` in this template repo | OSS template must stay generic | Keep your plugin in YOUR repo. The overlay setup script drops it into the per-instance `PLUGINS_DIR` at runtime | +| Port 8082 hardcoded in tests | Breaks the moment two overlays run | Read `${WORDPRESS_PORT:-8082}` from env in your test scripts | +| Modifying `docker-compose.yml` per project | Causes merge conflicts; template loses genericity | Override via env vars in `.env.overlay` (the compose file already uses `${VAR:-default}` for everything that matters) | + +--- + +## What you can change in this repo (and what you cannot) + +| Change | OK? | Where to do it instead | +|--------|-----|------------------------| +| Fix a bug in `check-ports.sh` (e.g. handle IPv6 better) | ✅ yes | here | +| Add a new generic option (`REDIS_PORT`, `MEMCACHED_PORT`, …) | ✅ yes — but parametrize via env var | here | +| Add a new docker compose service that every overlay would want (e.g. wp-mailhog already there, maybe a reverse proxy) | ✅ yes — but parametrized + opt-in via env | here | +| Add a plugin you need for YOUR project | ❌ no | your overlay's `config/plugins.yaml` | +| Add a test script for YOUR plugin | ❌ no | your overlay's `tests/` | +| Commit a premium plugin ZIP | ❌ no — licensing + repo bloat | your overlay's `bundles/` (gitignored), or the user provides a download link | +| Hardcode a port for one project | ❌ no | overlay's `.env.overlay` | + +When in doubt: this repo MUST install + run for any consumer using only the .env.example defaults. Nothing project-specific. + +--- + +## Quick smoke test if you ARE modifying this template + +```bash +cd ~/Documents/repositories/github/wp-test-env +git checkout main +git pull +cp .env.example .env +./scripts/setup.sh # should fully bootstrap, no errors +curl -sf http://localhost:8082 # WP responds 200 +./scripts/check-ports.sh # all ports allocated to OUR stack now in use +docker compose down -v # clean teardown +``` + +If any of those break, your change is not landable. + +--- + +## References + +- `docs/overlay-pattern.md` — full convention (port bands, naming, decision log) +- `README.md` — user-facing setup walkthrough +- `CHANGELOG.md` — what's new +- `.env.example` — defaults + per-field documentation +- `scripts/check-ports.sh` — port pre-flight (called by setup.sh) diff --git a/docker-compose.yml b/docker-compose.yml index 2379c03..b1bdf59 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -63,9 +63,9 @@ services: @ini_set('error_log', '/var/www/html/wp-content/debug.log'); volumes: - wordpress_data:/var/www/html - - ./plugins:/var/www/html/wp-content/plugins:cached - - ./themes:/var/www/html/wp-content/themes:cached - - ./uploads:/var/www/html/wp-content/uploads:cached + - ${PLUGINS_DIR:-./plugins}:/var/www/html/wp-content/plugins:cached + - ${THEMES_DIR:-./themes}:/var/www/html/wp-content/themes:cached + - ${UPLOADS_DIR:-./uploads}:/var/www/html/wp-content/uploads:cached - ./docker/config/php.ini:/usr/local/etc/php/conf.d/custom.ini:ro ports: - "${WORDPRESS_PORT:-8082}:80" @@ -83,9 +83,9 @@ services: - wordpress volumes: - wordpress_data:/var/www/html - - ./plugins:/var/www/html/wp-content/plugins:cached - - ./themes:/var/www/html/wp-content/themes:cached - - ./uploads:/var/www/html/wp-content/uploads:cached + - ${PLUGINS_DIR:-./plugins}:/var/www/html/wp-content/plugins:cached + - ${THEMES_DIR:-./themes}:/var/www/html/wp-content/themes:cached + - ${UPLOADS_DIR:-./uploads}:/var/www/html/wp-content/uploads:cached - ./scripts:/scripts:ro environment: WORDPRESS_DB_HOST: mysql:3306 diff --git a/docs/overlay-pattern.md b/docs/overlay-pattern.md new file mode 100644 index 0000000..a86627d --- /dev/null +++ b/docs/overlay-pattern.md @@ -0,0 +1,163 @@ +# Overlay Pattern — running multiple wp-test-env instances in parallel + +wp-test-env is a generic WordPress testing harness. Plugin/theme projects (overlays) +sit on top of it: capacium-bridge-tests, elementeer-tests, etc. Two or more +overlays must be able to run in parallel on the same machine without colliding. + +This doc codifies how an overlay declares its identity and how it stays out of +other overlays' way. + +## TL;DR + +An overlay ships a single `.env.overlay` file (or any `.env` it tells the user +to copy) that sets at minimum: + +```bash +# Kennung (outside-visible identifier) +COMPOSE_PROJECT_NAME=capacium-wptest # MUST be unique per overlay +COMPOSE_PROJECT_DIR=envs/capacium-bridge-tests # MUST be unique per overlay + +# Ports — MUST be free; check-ports.sh fails fast otherwise +WORDPRESS_PORT=8083 +MYSQL_PORT=3307 +PHPMYADMIN_PORT=8084 +MAILHOG_SMTP_PORT=1026 +MAILHOG_WEB_PORT=8026 +``` + +Then the overlay's setup script does: + +```bash +cd /path/to/wp-test-env +cp /path/to/overlay/.env.overlay .env +./scripts/check-ports.sh # fail fast if any port is taken +./scripts/setup.sh # start the stack +``` + +That's it. Each overlay's containers carry the `${COMPOSE_PROJECT_NAME}-` prefix +and live in isolated docker networks/volumes. Two overlays with different +COMPOSE_PROJECT_NAME + non-colliding ports coexist cleanly. + +## Naming convention + +- `COMPOSE_PROJECT_NAME` — kebab-case, suffixed `-wptest` to make it obvious + this is a wp-test-env-derived stack: + - `elementeer-wptest` (or just `wptesting` = default) + - `capacium-wptest` + - `myplugin-wptest` +- `COMPOSE_PROJECT_DIR` — `envs/` keeps per-overlay state + (uploads, logs, reports) under wp-test-env's `envs/` folder. State stays + isolated even if the same `wp-test-env/` checkout is reused. + +## Port allocation strategy + +Each overlay picks a port band offset by +1 from the previous overlay's: + +| Slot | Default (`wptesting`) | Suggested second (`capacium-wptest`) | Suggested third | +|------|----------------------|--------------------------------------|------------------| +| WORDPRESS_PORT | 8082 | 8083 | 8084 | +| MYSQL_PORT | 3306 | 3307 | 3308 | +| PHPMYADMIN_PORT| 8092 | 8093 | 8094 | +| MAILHOG_SMTP_PORT | 1025 | 1026 | 1027 | +| MAILHOG_WEB_PORT | 8025 | 8026 | 8027 | + +Pick the next free slot when you set up a new overlay. The collision check +catches accidents. + +Note: `PHPMYADMIN_PORT` default in `.env.example` is 8083 (one above WP). +For consistency we recommend bands of +10 between adjacent overlays so each +overlay's 5 ports stay grouped — but for a 2-overlay setup, +1 is fine. + +## How an overlay should be structured + +``` +-tests/ +├── README.md ← how to use this overlay +├── AGENTS.md ← agent-readable usage notes +├── wp-testing-env/ +│ ├── .env.overlay ← THE overlay manifest (the "kennung") +│ ├── config/ +│ │ └── plugins.yaml ← plugin profile for this overlay +│ ├── bundles/ ← premium plugin ZIPs (gitignored) +│ ├── scripts/ +│ │ └── setup-overlay.sh ← deploy plugin from local source + activate +│ └── tests/ +│ ├── smoke-test.sh ← uses ${WORDPRESS_PORT:-8082} from env +│ ├── ... +``` + +The `.env.overlay` file is the kennung. The setup script copies it to +`wp-test-env/.env`, then runs `check-ports.sh` + `setup.sh`. Tests read +their target URL from `WORDPRESS_PORT` so they hit the right overlay. + +## Coexisting with another overlay + +To run capacium-bridge-tests alongside elementeer-tests on the same machine: + +1. **Pick non-colliding ports** for each overlay (see table above). +2. **Pick distinct COMPOSE_PROJECT_NAME** for each (e.g. `elementeer-wptest` + and `capacium-wptest`). +3. **Pick distinct COMPOSE_PROJECT_DIR** for each (e.g. + `envs/elementeer-tests` and `envs/capacium-bridge-tests`). +4. **Run `check-ports.sh` before each setup**. If the elementeer stack is + already running, the capacium-wptest setup's check will detect any port + collision and fail with a clear message. + +After both are up, `docker ps` shows them as separate stacks: + +``` +$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}' +NAMES IMAGE PORTS +elementeer-wptest-wordpress wordpress:latest 0.0.0.0:8082->80/tcp +elementeer-wptest-mysql mysql:8.0 0.0.0.0:3306->3306/tcp +capacium-wptest-wordpress wordpress:latest 0.0.0.0:8083->80/tcp +capacium-wptest-mysql mysql:8.0 0.0.0.0:3307->3306/tcp +``` + +Tear down one without affecting the other: + +```bash +docker compose --env-file .env -p capacium-wptest down -v # only capacium +docker compose --env-file .env -p elementeer-wptest down -v # only elementeer +``` + +## Port collision check + +`scripts/check-ports.sh` (in wp-test-env) probes the host BEFORE docker compose +up. It: + +- Reads ports from `.env` (or accepts explicit args). +- For each port: checks Docker containers first (`docker ps -f publish=`), + then host processes (`lsof -i:`). +- Exits 0 if all free; exits 1 with a clear error message listing each + occupied port + what's holding it. + +Integrate it into your overlay's setup script: + +```bash +# Pre-flight before docker compose up +"${WP_TEST_ENV_ROOT}/scripts/check-ports.sh" || { + err "Cannot start ${MY_OVERLAY} — port collision (see above)" + exit 1 +} +``` + +## Decision log: why this design + +- **`COMPOSE_PROJECT_NAME` over docker-compose `name:` field**: env-var is + inherited by ALL `docker compose` invocations in the same shell, including + ad-hoc `docker compose down -v` commands. The compose-file `name:` is also + fine but requires modifying the file per overlay. +- **State subdir under `envs/`** over fork-per-overlay: a single wp-test-env + checkout can host multiple overlays without git divergence. +- **Port check as pre-flight script** over silent docker bind-mount failures: + Docker errors on port conflict are noisy and don't tell you what's holding + the port. Explicit script gives actionable output. + +## See also + +- `scripts/check-ports.sh` — the port-collision pre-flight +- `scripts/setup.sh` — main bootstrap; reads .env, starts stack +- `.env.example` — default values for the primary instance +- `docs/multi-instance-coexistence.md` (planned) — operations runbook for + managing multiple overlays in CI / on dev machines diff --git a/plugins/debug-bar.zip b/plugins/debug-bar.zip deleted file mode 100644 index 8bdf6f0..0000000 Binary files a/plugins/debug-bar.zip and /dev/null differ diff --git a/plugins/query-monitor.zip b/plugins/query-monitor.zip deleted file mode 100644 index acfaa6f..0000000 Binary files a/plugins/query-monitor.zip and /dev/null differ diff --git a/plugins/user-switching.zip b/plugins/user-switching.zip deleted file mode 100644 index 2819aa4..0000000 Binary files a/plugins/user-switching.zip and /dev/null differ diff --git a/plugins/wordpress-importer.zip b/plugins/wordpress-importer.zip deleted file mode 100644 index 678e24b..0000000 Binary files a/plugins/wordpress-importer.zip and /dev/null differ diff --git a/scripts/check-ports.sh b/scripts/check-ports.sh new file mode 100755 index 0000000..10afe41 --- /dev/null +++ b/scripts/check-ports.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# ============================================================================= +# wp-test-env — Port collision check (pre-flight) +# ============================================================================= +# Fails fast if any port declared in the current .env (or passed as args) is +# already in use by another Docker container or another process on the host. +# +# Usage: +# ./scripts/check-ports.sh # checks ports from .env +# ./scripts/check-ports.sh 8082 3306 8083 1025 8025 # checks explicit ports +# ./scripts/check-ports.sh --quiet # only print on failure +# +# Exit codes: +# 0 — all clear +# 1 — at least one port is occupied +# 2 — could not determine port state (lsof missing, etc.) +# +# Design intent: a project running this script with its own COMPOSE_PROJECT_NAME +# (a "kennung") must NOT collide with any other project's running stack. The +# user-visible signal is the project_name + the ports; collisions on either +# break the multi-instance promise. +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" + +QUIET=false +if [ "${1:-}" = "--quiet" ]; then + QUIET=true + shift +fi + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +log() { $QUIET || echo -e "${GREEN}[check-ports]${NC} $1"; } +warn() { $QUIET || echo -e "${YELLOW}[check-ports]${NC} $1"; } +err() { echo -e "${RED}[check-ports]${NC} $1" >&2; } + +# ── Collect ports to check ────────────────────────────────────────────────── +declare -a PORTS_TO_CHECK +declare -A PORT_LABELS # port → human-readable label + +if [ $# -gt 0 ]; then + # Ports passed as args + for p in "$@"; do + PORTS_TO_CHECK+=("$p") + PORT_LABELS[$p]="(arg)" + done +else + # Read from .env in repo root + ENV_FILE="${ROOT_DIR}/.env" + if [ ! -f "$ENV_FILE" ]; then + err "no .env found at $ENV_FILE and no port args given" + exit 2 + fi + # shellcheck disable=SC1090 + set -a; source "$ENV_FILE"; set +a + + for var in WORDPRESS_PORT MYSQL_PORT PHPMYADMIN_PORT MAILHOG_SMTP_PORT MAILHOG_WEB_PORT; do + v="${!var:-}" + if [ -n "$v" ]; then + PORTS_TO_CHECK+=("$v") + PORT_LABELS[$v]="$var" + fi + done +fi + +if [ ${#PORTS_TO_CHECK[@]} -eq 0 ]; then + warn "no ports to check (.env declares none, no args)" + exit 0 +fi + +PROJECT_NAME="${COMPOSE_PROJECT_NAME:-(default)}" +log "Project: ${PROJECT_NAME}" +log "Checking ports: ${PORTS_TO_CHECK[*]}" + +# ── Probe each port ────────────────────────────────────────────────────────── +OCCUPIED=() +declare -A OCCUPIED_BY + +for port in "${PORTS_TO_CHECK[@]}"; do + # Docker check first (most informative — tells us which container) + container=$(docker ps --format '{{.Names}} {{.Ports}}' 2>/dev/null \ + | awk -v p=":${port}->" '$0 ~ p { print $1; exit }') + if [ -n "$container" ]; then + OCCUPIED+=("$port") + OCCUPIED_BY[$port]="docker container: $container" + continue + fi + + # Host-level check via lsof (preferred) or netstat fallback + in_use=false + if command -v lsof >/dev/null 2>&1; then + if lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then + in_use=true + who=$(lsof -nP -iTCP:"$port" -sTCP:LISTEN -F c 2>/dev/null | sed -n 's/^c//p' | head -1) + OCCUPIED_BY[$port]="host process: ${who:-unknown}" + fi + elif command -v ss >/dev/null 2>&1; then + if ss -ltn 2>/dev/null | awk -v p=":${port}" '$4 ~ p' | grep -q .; then + in_use=true + OCCUPIED_BY[$port]="host process (ss; pid unknown — install lsof for detail)" + fi + elif command -v netstat >/dev/null 2>&1; then + if netstat -ltn 2>/dev/null | awk -v p=":${port}" '$4 ~ p' | grep -q .; then + in_use=true + OCCUPIED_BY[$port]="host process (netstat; pid unknown — install lsof for detail)" + fi + else + warn "no lsof/ss/netstat available — cannot verify port $port" + continue + fi + + if $in_use; then + OCCUPIED+=("$port") + fi +done + +# ── Report ─────────────────────────────────────────────────────────────────── +if [ ${#OCCUPIED[@]} -eq 0 ]; then + log "✓ All ${#PORTS_TO_CHECK[@]} port(s) free" + exit 0 +fi + +err "" +err "Port collision detected — cannot start ${PROJECT_NAME} stack" +err "" +for port in "${OCCUPIED[@]}"; do + label="${PORT_LABELS[$port]:-(unknown)}" + by="${OCCUPIED_BY[$port]:-unknown}" + err " ✗ port $port (${label}) → in use by ${by}" +done +err "" +err "Resolutions:" +err " 1. Change the conflicting port in .env (e.g. WORDPRESS_PORT=8085)" +err " 2. Stop the other process / container holding the port" +err " 3. Run the conflicting overlay's teardown (docker compose -p down)" +err "" +exit 1 diff --git a/scripts/setup.sh b/scripts/setup.sh index c8263c2..0044854 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -99,6 +99,54 @@ EOF # Redirect compose file to use project-specific volumes/paths COMPOSE_FILE="${ROOT_DIR}/docker-compose.yml" fi + + # ── Per-instance bind-mount paths (CRITICAL for isolation) ───────────── + # If PLUGINS_DIR / THEMES_DIR / UPLOADS_DIR are set to overlay-specific + # paths, create them so the overlay's bind mounts have a real target. + # The template (this repo) intentionally does NOT ship plugins — overlays + # are responsible for declaring their own via config/plugins.yaml + + # setup.sh's plugin installer (which reads PLUGINS_CONFIG and pulls + # from wordpress.org / local / URL). + # + # An overlay's setup script that calls this setup.sh as a sub-step + # MAY pre-populate the per-instance plugins dir with extra plugins + # (e.g. capacium-bridge dropped in from a sibling source dir) BEFORE + # this function runs — we just guarantee the dir exists, we don't + # touch its contents. + for dir_var in PLUGINS_DIR THEMES_DIR UPLOADS_DIR; do + v="${!dir_var:-}" + # Skip the default (means: same as repo root; nothing to provision) + case "$v" in ""|"./plugins"|"./themes"|"./uploads") continue ;; esac + + full_path="${ROOT_DIR}/${v#./}" + if [ ! -d "$full_path" ]; then + mkdir -p "$full_path" + info " Created per-instance $dir_var → $v" + fi + done +} + +# ── Port collision pre-flight ──────────────────────────────────────────────── +check_ports() { + local check_script="${SCRIPT_DIR}/check-ports.sh" + if [ ! -x "$check_script" ]; then + warn "scripts/check-ports.sh missing or not executable — skipping pre-flight" + return 0 + fi + info "Pre-flight: checking ports..." + # On --fresh, own containers will be torn down; allow self-collision + if $FRESH_START; then + info " (--fresh: skipping check; will tear down own containers first)" + return 0 + fi + if ! "$check_script" --quiet; then + # Re-run verbose to show the user what's wrong + "$check_script" || true + err "" + err "Port collision blocks startup. See $check_script for resolution hints." + exit 1 + fi + info " ✓ all ports free" } # ── Docker ─────────────────────────────────────────────────────────────────── @@ -107,6 +155,7 @@ start_containers() { warn "Fresh start — destroying existing volumes" $COMPOSE_CMD -f "$COMPOSE_FILE" down -v 2>/dev/null || true fi + check_ports info "Starting Docker containers..." $COMPOSE_CMD -f "$COMPOSE_FILE" up -d --wait info "All containers are running."