diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..20c854d --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# Copy to .env to override defaults. All values are optional. + +# Host port for the dev site (http://localhost:8080) +WP_PORT=8080 +# Host port for phpMyAdmin (`make tools`) +PMA_PORT=8081 + +# PHP version for the dev site image (any wordpress:php-apache tag) +WP_PHP_VERSION=8.2 + +# Cablecast server the dev site syncs from +CABLECAST_SERVER=https://rays-house.cablecast.net + +# Xdebug mode for the dev site: off, debug, profile, develop, ... +XDEBUG_MODE=debug + +# Default PHP / WordPress versions for `make test` +PHP_VERSION=8.2 +WP_VERSION=latest diff --git a/.gitignore b/.gitignore index 0181ca1..c7bf86d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,8 @@ composer.lock # IDE .idea/ -.vscode/ +.vscode/* +!.vscode/launch.json *.swp *.swo @@ -21,3 +22,6 @@ Thumbs.db # Logs *.log + +# Docker dev environment +.env diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d82af36 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for Xdebug (docker)", + "type": "php", + "request": "launch", + "port": 9003, + "pathMappings": { + "/var/www/html/wp-content/plugins/wp-cablecast": "${workspaceFolder}" + } + } + ] +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..717bf75 --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +-include .env + +# Local development helpers. See "Local Development" in README.md. + +PHP ?= $(or $(PHP_VERSION),8.2) +WP ?= $(or $(WP_VERSION),latest) +WP_PORT ?= 8080 +TS_PORT ?= 8443 + +COMPOSE = docker compose +WPCLI = $(COMPOSE) run --rm cli + +.PHONY: up down setup wp sync logs debug-log tools tailscale tailscale-off test test-matrix test-clean reset + +up: ## Start the dev site and install WordPress on first run + $(COMPOSE) up -d --build + $(WPCLI) sh wp-content/plugins/wp-cablecast/bin/dev-setup.sh + +down: ## Stop containers (keeps data) + $(COMPOSE) --profile tools down + +setup: ## Re-run WordPress install / plugin activation + $(WPCLI) sh wp-content/plugins/wp-cablecast/bin/dev-setup.sh + +wp: ## Run WP-CLI: make wp ARGS="plugin list" + $(WPCLI) wp $(ARGS) + +sync: ## Run the Cablecast sync now instead of waiting for cron + $(WPCLI) wp cron event run cablecast_cron_hook + +logs: ## Tail container logs + $(COMPOSE) logs -f + +debug-log: ## Tail wp-content/debug.log + $(COMPOSE) exec wordpress tail -f wp-content/debug.log + +tools: ## Start phpMyAdmin on http://localhost:8081 + $(COMPOSE) --profile tools up -d phpmyadmin + +tailscale: ## Share the dev site on your tailnet over HTTPS (port TS_PORT, default 8443) + tailscale serve --bg --https=$(TS_PORT) http://localhost:$(WP_PORT) + +tailscale-off: ## Stop sharing the dev site on your tailnet + tailscale serve --https=$(TS_PORT) off + +test: ## Run PHPUnit: make test [PHP=7.4] [WP=6.2] [ARGS="--filter Foo"] + PHP_VERSION=$(PHP) WP_VERSION=$(WP) $(COMPOSE) run --rm --build tests bash bin/docker-test.sh $(ARGS) + +test-matrix: ## Run the same PHP x WP matrix as CI + @set -e; for php in 7.4 8.0 8.1 8.2; do for wp in 6.2 6.4 latest; do \ + $(MAKE) --no-print-directory test PHP=$$php WP=$$wp; \ + done; done + +test-clean: ## Drop cached WP test suites (e.g. to pick up a new "latest") + $(COMPOSE) down + docker volume rm $$(basename $(CURDIR))_test-cache + +reset: ## Destroy everything, including the database + $(COMPOSE) --profile tools --profile cli --profile tests down -v diff --git a/README.md b/README.md index 95a2a69..df9ec21 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,47 @@ I've set up a simple demo site that I'll be expanding with examples of how to do ## Installation Upload the plugin and activate it. Then in the admin dashboard navigate to `Cablecast Settings`. There is a single input for the `Cablecast Server Address`. This is the url you use to login to Cablecast from the public internet. Example `http://tighty.tv`. +## Local Development +A Docker based dev environment is included. It runs WordPress with this repo mounted as the plugin, a MariaDB database, a container that runs WP-Cron every minute (instead of during page loads), and a PHPUnit runner. + +Requirements: [Docker Desktop](https://www.docker.com/products/docker-desktop/) (or another Docker runtime with `docker compose`) and `make`. PHP and Composer are not needed on your machine. + +```sh +make up # build, start, install WordPress, activate the plugin +``` + +- Site: http://localhost:8080 +- Admin: http://localhost:8080/wp-admin (`admin` / `admin`) +- The plugin is pre-configured to sync from `https://rays-house.cablecast.net`. Copy `.env.example` to `.env` to change the server, ports, or PHP version, then run `make up` again. + +Common commands: + +|Command|Description| +|---|---| +|`make sync`|Run the Cablecast sync now instead of waiting for cron| +|`make wp ARGS="post list --post_type=show"`|Run any WP-CLI command| +|`make debug-log`|Tail `wp-content/debug.log`| +|`make logs`|Tail container logs| +|`make tools`|Start phpMyAdmin on http://localhost:8081| +|`make tailscale`|Share the site on your tailnet at `https://..ts.net:8443` (`make tailscale-off` to stop)| +|`make down`|Stop containers (keeps data)| +|`make reset`|Delete containers and all data| + +WordPress uses whatever host it's reached by, so the site also works behind a proxy such as Tailscale Serve or ngrok without changing its URL. `make tailscale` uses [Tailscale Serve](https://tailscale.com/kb/1312/serve), which also gets around the macOS firewall blocking direct tailnet connections to Docker ports. + +### Tests +```sh +make test # PHP 8.2, latest WordPress +make test PHP=7.4 WP=6.2 # any version CI tests against +make test ARGS="--filter SyncLock" # pass args to PHPUnit +make test-matrix # the full CI matrix +``` + +The WordPress test suite for each version is cached in a Docker volume. Run `make test-clean` to re-download it (for example, to pick up a new `latest`). + +### Debugging +Xdebug is installed in the WordPress container and starts when a request carries the `XDEBUG_TRIGGER` cookie (set it with a browser extension such as Xdebug Helper). It connects to port 9003. `.vscode/launch.json` contains the path mappings for VS Code's PHP Debug extension. + ## Important Notes About cron_schedules TL;DR - Follow these instructions for setting up wordpress cron: https://developer.wordpress.org/plugins/cron/hooking-into-the-system-task-scheduler/ diff --git a/bin/dev-setup.sh b/bin/dev-setup.sh new file mode 100755 index 0000000..ce57de5 --- /dev/null +++ b/bin/dev-setup.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env sh +# Installs WordPress and activates the plugin in the docker dev environment. +# Runs inside the `cli` container (`make setup`); safe to re-run. +set -e + +cd /var/www/html + +echo "Waiting for wp-config.php..." +i=0 +until [ -f wp-config.php ]; do + i=$((i + 1)) + if [ $i -gt 60 ]; then + echo "wp-config.php never appeared; is the wordpress container running?" >&2 + exit 1 + fi + sleep 1 +done + +if ! wp core is-installed 2>/dev/null; then + wp core install \ + --url="http://localhost:${WP_PORT:-8080}" \ + --title="Cablecast Dev" \ + --admin_user=admin \ + --admin_password=admin \ + --admin_email=admin@example.com \ + --skip-email +fi + +# The wordpress image ships a rewrite-ready .htaccess, so no --hard needed. +wp rewrite structure '/%postname%/' +wp plugin activate wp-cablecast + +if [ -n "$CABLECAST_SERVER" ]; then + wp option patch update cablecast_options server "$CABLECAST_SERVER" 2>/dev/null \ + || wp option patch insert cablecast_options server "$CABLECAST_SERVER" +fi + +echo +echo "Site: http://localhost:${WP_PORT:-8080}" +echo "Admin: http://localhost:${WP_PORT:-8080}/wp-admin (admin / admin)" +echo "Cablecast server: $(wp option pluck cablecast_options server)" diff --git a/bin/docker-test.sh b/bin/docker-test.sh new file mode 100755 index 0000000..2c49cec --- /dev/null +++ b/bin/docker-test.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Runs the PHPUnit suite inside the `tests` container (`make test`). +# WordPress core + test library are cached per WP version in the test-cache volume. +set -e + +WP_VERSION=${WP_VERSION:-latest} +PHP_VERSION=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;') + +export WP_CORE_DIR=/cache/wp/$WP_VERSION/core +export WP_TESTS_DIR=/cache/wp/$WP_VERSION/tests-lib + +echo "PHP $PHP_VERSION / WordPress $WP_VERSION" + +# composer.lock isn't committed, so re-resolve when the PHP version changes. +if [ "$(cat vendor/.php-version 2>/dev/null)" != "$PHP_VERSION" ]; then + composer update --prefer-dist --no-progress --no-interaction + echo "$PHP_VERSION" > vendor/.php-version +fi + +if [ ! -f "$WP_TESTS_DIR/wp-tests-config.php" ]; then + bash bin/install-wp-tests.sh wordpress_test root root db "$WP_VERSION" true +fi + +composer test -- "$@" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5f33636 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,128 @@ +# Local development environment. See "Local Development" in README.md. + +x-wp-env: &wp-env + WORDPRESS_DB_HOST: db + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: wordpress + WORDPRESS_DB_NAME: wordpress + WORDPRESS_DEBUG: 1 + WORDPRESS_CONFIG_EXTRA: | + define('WP_ENVIRONMENT_TYPE', 'local'); + define('WP_DEBUG_LOG', true); + define('WP_DEBUG_DISPLAY', false); + define('DISABLE_WP_CRON', true); + // Use whatever host the site is reached by (localhost, a Tailscale/ngrok proxy, ...). + if (!empty($$_SERVER['HTTP_HOST'])) { + $$https = (!empty($$_SERVER['HTTPS']) && $$_SERVER['HTTPS'] !== 'off') + || ($$_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'; + if ($$https) { $$_SERVER['HTTPS'] = 'on'; } + define('WP_HOME', ($$https ? 'https' : 'http') . '://' . $$_SERVER['HTTP_HOST']); + define('WP_SITEURL', WP_HOME); + } + +x-wp-volumes: &wp-volumes + - wordpress:/var/www/html + - .:/var/www/html/wp-content/plugins/wp-cablecast + +x-wp-cli: &wp-cli + image: wordpress:cli + # Match the apache image's www-data uid so files in the shared volume stay writable. + user: "33:33" + environment: + <<: *wp-env + WP_CLI_CACHE_DIR: /tmp/wp-cli-cache + WP_PORT: ${WP_PORT:-8080} + CABLECAST_SERVER: ${CABLECAST_SERVER:-https://rays-house.cablecast.net} + volumes: *wp-volumes + depends_on: + db: + condition: service_healthy + wordpress: + condition: service_started + +services: + db: + # 11.4+ enables TLS out of the box, which WP-CLI's MariaDB client requires. + image: mariadb:11.8 + environment: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: wordpress + MARIADB_USER: wordpress + MARIADB_PASSWORD: wordpress + volumes: + - db:/var/lib/mysql + - ./docker/db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 5s + timeout: 5s + retries: 20 + + wordpress: + build: + context: docker/wordpress + args: + PHP_VERSION: ${WP_PHP_VERSION:-8.2} + ports: + - "${WP_PORT:-8080}:80" + environment: + <<: *wp-env + XDEBUG_MODE: ${XDEBUG_MODE:-debug} + volumes: *wp-volumes + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + db: + condition: service_healthy + + # One-off WP-CLI commands: `make wp ARGS="plugin list"` + cli: + <<: *wp-cli + profiles: ["cli"] + + # Runs WP-Cron from outside web requests, as the README recommends for production. + cron: + <<: *wp-cli + command: > + sh -c 'while true; do + wp core is-installed 2>/dev/null && wp cron event run --due-now --quiet; + sleep 60; + done' + restart: unless-stopped + + # PHPUnit runner: `make test` / `make test PHP=7.4 WP=6.2` + tests: + build: + context: docker/tests + args: + PHP_VERSION: ${PHP_VERSION:-8.2} + image: wp-cablecast-tests:php${PHP_VERSION:-8.2} + profiles: ["tests"] + environment: + WP_VERSION: ${WP_VERSION:-latest} + COMPOSER_CACHE_DIR: /cache/composer + volumes: + - .:/app + - test-cache:/cache + command: bash bin/docker-test.sh + depends_on: + db: + condition: service_healthy + + phpmyadmin: + image: phpmyadmin:5 + profiles: ["tools"] + ports: + - "${PMA_PORT:-8081}:80" + environment: + PMA_HOST: db + PMA_USER: root + PMA_PASSWORD: root + depends_on: + db: + condition: service_healthy + +volumes: + db: + wordpress: + test-cache: diff --git a/docker/db/init.sql b/docker/db/init.sql new file mode 100644 index 0000000..a6eb662 --- /dev/null +++ b/docker/db/init.sql @@ -0,0 +1,4 @@ +-- The dev site database (`wordpress`) is created by the image; this adds the +-- database used by the PHPUnit suite. +CREATE DATABASE IF NOT EXISTS wordpress_test; +GRANT ALL PRIVILEGES ON wordpress_test.* TO 'wordpress'@'%'; diff --git a/docker/tests/Dockerfile b/docker/tests/Dockerfile new file mode 100644 index 0000000..895b0f9 --- /dev/null +++ b/docker/tests/Dockerfile @@ -0,0 +1,17 @@ +ARG PHP_VERSION=8.2 +FROM php:${PHP_VERSION}-cli + +# Older PHP images (7.4, 8.0) are built on Debian bullseye, which is EOL and +# only available from the archive. +RUN if grep -q bullseye /etc/os-release; then \ + sed -i -e 's|deb.debian.org|archive.debian.org|g' -e '/bullseye-updates/d' /etc/apt/sources.list; \ + fi + +RUN apt-get update \ + && apt-get install -y --no-install-recommends git unzip subversion \ + && rm -rf /var/lib/apt/lists/* \ + && docker-php-ext-install mysqli + +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + +WORKDIR /app diff --git a/docker/wordpress/Dockerfile b/docker/wordpress/Dockerfile new file mode 100644 index 0000000..33f9146 --- /dev/null +++ b/docker/wordpress/Dockerfile @@ -0,0 +1,7 @@ +ARG PHP_VERSION=8.2 +FROM wordpress:php${PHP_VERSION}-apache + +RUN pecl install xdebug \ + && docker-php-ext-enable xdebug + +COPY xdebug.ini /usr/local/etc/php/conf.d/zz-xdebug.ini diff --git a/docker/wordpress/xdebug.ini b/docker/wordpress/xdebug.ini new file mode 100644 index 0000000..e123759 --- /dev/null +++ b/docker/wordpress/xdebug.ini @@ -0,0 +1,6 @@ +; Mode comes from the XDEBUG_MODE env var (see docker-compose.yml). +; Start a session with the browser extension / XDEBUG_TRIGGER cookie. +xdebug.start_with_request=trigger +xdebug.client_host=host.docker.internal +xdebug.client_port=9003 +xdebug.log_level=0