Files
nolimit-discounts/ci/site-test.sh
T

237 lines
11 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# Stand up a throwaway WordPress + WooCommerce, install this plugin into it,
# and run the site tests from ci/plugin.conf inside it.
#
# WP_VERSION=latest|nightly|6.8 WC_VERSION=latest|beta|11.1.0 PHP_VERSION=8.3
#
# The point of the version knobs is the question this whole directory exists to
# answer: *will the shop still work after I press update?* Running the same
# tests against `latest` and against `nightly`/`beta` answers it before the
# update happens rather than after.
#
# Written for a Gitea Actions job, which runs INSIDE a container that talks to
# the HOST's docker daemon. A bind mount of $PWD there mounts an empty
# directory nobody created, and the symptom is "Could not open input file",
# which reads like a typo in the path. So the plugin goes in over `docker cp`
# and all state lives in named volumes. That also makes the script behave
# identically on GitHub's runners and on a laptop.
set -euo pipefail
cd "$(dirname "$0")/.."
. ci/lib.sh
ci_load_conf
WP_VERSION="${WP_VERSION:-latest}"
WC_VERSION="${WC_VERSION:-latest}"
PHP_VERSION="${PHP_VERSION:-8.3}"
# 🔴 NOT just $$. A Gitea job runs in a fresh container where the shell gets a
# low PID, and those REPEAT across runs — so "wpci-$$" collides with the
# leftovers of any earlier run that did not clean up, and the job dies on
# `network with name wpci-147-net already exists`. Measured on Gitea run 999:
# every run landing on PID 147 was wedged permanently, while the same script on
# a laptop never collides because PIDs there are large and varied.
#
# The epoch and $RANDOM make the name unique per run even inside a container
# that always starts at the same PID.
id="wpci-$$-$(date +%s)-${RANDOM:-0}"
net="$id-net"
# The name is unique to this run, so anything already using it is debris from a
# crashed run and is ours to clear. Without this a single wedged name stays
# wedged forever, which is exactly what happened on the Gitea runner.
docker rm -f -v "$id-db" "$id-cli" >/dev/null 2>&1 || true
docker network rm "$net" >/dev/null 2>&1 || true
# 🔴 -v, and it is the whole bug. `mariadb:lts` declares VOLUME
# /var/lib/mysql, so every run creates an ANONYMOUS volume, and `docker rm -f`
# does not remove anonymous volumes — only `docker rm -f -v` does. The named
# $id-wp below is removed correctly, which is exactly what made this function
# look complete: removing three of the four things you created is
# indistinguishable from removing all four unless you count.
#
# Measured 2026-09-17 after a sibling plugin traced it: the Gitea runner's disk
# was 100% full, 26 GB in 95 volumes with 1 in use, and a push there failed
# with `unable to create temporary object directory` — three layers from the
# cause. This machine had 127 dangling volumes and 15.2 GB reclaimable.
cleanup() {
# Ask the containers which volumes they own BEFORE removing them. A global
# before/after count would be wrong here: several sessions share this box and
# run these container pairs concurrently, so a neighbour's volume would read
# as this run's leak.
local owned=""
owned=$(docker inspect -f \
'{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}} {{end}}{{end}}' \
"$id-db" "$id-cli" 2>/dev/null || true)
docker rm -f -v "$id-db" "$id-cli" >/dev/null 2>&1 || true
docker volume rm -f "$id-wp" >/dev/null 2>&1 || true
docker network rm "$net" >/dev/null 2>&1 || true
# Count rather than trust the flag. This is advisory on a failed run — a test
# that died halfway has more urgent news — but on a successful run a leftover
# volume is a real regression and says so.
local leaked=""
for v in $owned; do
docker volume inspect "$v" >/dev/null 2>&1 && leaked="$leaked $v"
done
if [ -n "$leaked" ]; then
echo "::warning::This run left docker volume(s) behind:$leaked — 95 of these filled the Gitea runner's disk."
# `return` from an EXIT trap does NOT change the script's exit status;
# only `exit` does. Without this the warning would print and the build
# would still go green, which is the failure mode of every other check
# this kit has had to fix tonight.
if [ "${site_test_ok:-0}" = 1 ]; then
ci_fail "The site test leaked the volume(s) above. Left alone this fills the runner's disk, and the symptom is a push failing with 'unable to create temporary object directory'."
exit 1
fi
fi
}
trap cleanup EXIT
echo "WordPress=$WP_VERSION WooCommerce=$WC_VERSION PHP=$PHP_VERSION plugin=$PLUGIN_SLUG"
ci_step "database"
docker network create "$net" >/dev/null
docker volume create "$id-wp" >/dev/null
docker run -d --name "$id-db" --network "$net" \
-e MARIADB_ROOT_PASSWORD=root -e MARIADB_DATABASE=wordpress \
-e MARIADB_USER=wordpress -e MARIADB_PASSWORD=wordpress mariadb:lts >/dev/null
# wordpress:cli runs as www-data (33); a fresh named volume is root-owned, so
# ownership is fixed from a root container before anything tries to write.
docker run --rm -v "$id-wp:/var/www/html" alpine chown 33:33 /var/www/html
docker run -d --name "$id-cli" --network "$net" -v "$id-wp:/var/www/html" \
-e WORDPRESS_DB_HOST="$id-db" "wordpress:cli-php$PHP_VERSION" sleep infinity >/dev/null
# The image ships no php.ini, so PHP's 128M default applies and `wp core
# download` dies unpacking the zip. WP_CLI_PHP_ARGS does not help here: the
# phar is executed directly, not through the wp launcher.
docker exec -u root "$id-cli" sh -c 'echo memory_limit=512M > /usr/local/etc/php/conf.d/zz-memory.ini'
wp() { docker exec "$id-cli" wp "$@"; }
for i in $(seq 1 60); do
docker exec "$id-db" mariadb -uwordpress -pwordpress -e 'select 1' wordpress >/dev/null 2>&1 && break
if [ "$i" = 60 ]; then echo "MariaDB never came up." >&2; exit 1; fi
sleep 2
done
ci_step "WordPress $WP_VERSION"
if [ "$WP_VERSION" = latest ]; then
wp core download --quiet
else
wp core download --version="$WP_VERSION" --quiet
fi
wp config create --dbname=wordpress --dbuser=wordpress --dbpass=wordpress \
--dbhost="$id-db" --quiet
# Deprecations are the way a WordPress or PHP upgrade actually breaks a plugin:
# nothing fails, a notice appears, and six months later it is a fatal. They are
# logged to a file (not the page) and checked at the end of this script.
wp config set WP_DEBUG true --raw --quiet
wp config set WP_DEBUG_LOG true --raw --quiet
wp config set WP_DEBUG_DISPLAY false --raw --quiet
wp core install --url=http://plugin.test --title="$PLUGIN_SLUG CI" \
--admin_user=admin --admin_password=admin --admin_email=ci@example.com \
--skip-email --quiet
echo " installed $(wp core version)"
if [ "$NEEDS_WOOCOMMERCE" = yes ]; then
ci_step "WooCommerce $WC_VERSION"
case "$WC_VERSION" in
latest)
wp plugin install woocommerce --activate --quiet
;;
beta)
# Resolved inside the container, which has PHP and network; the host
# runner is not guaranteed either.
target=$(docker exec -i -e SLUG=woocommerce "$id-cli" php < ci/latest-prerelease.php)
if [ -n "$target" ]; then
echo " prerelease available: $target"
wp plugin install woocommerce --version="$target" --activate --quiet
else
echo " no prerelease newer than stable right now — using the stable release"
wp plugin install woocommerce --activate --quiet
fi
;;
*)
wp plugin install woocommerce --version="$WC_VERSION" --activate --quiet
;;
esac
# HPOS, because that is what a shop set up in the last few years runs, and a
# plugin that touches orders behaves differently under it.
wp option update woocommerce_custom_orders_table_enabled yes >/dev/null
echo " installed $(wp plugin get woocommerce --field=version)"
fi
ci_step "$PLUGIN_SLUG"
# Exactly the files the release zip will contain, plus the site tests. Testing
# the working tree instead would test a plugin no shop ever receives: one
# unanchored .gitignore line is enough for a file to be present locally,
# absent from the release, and never missed until a customer installs it.
{ ./ci/dist-files.sh; printf '%s\n' ${SITE_TESTS:-}; } | tr '\n' '\0' | tar --null -T - -cf - \
| docker exec -i "$id-cli" sh -c \
"mkdir -p /var/www/html/wp-content/plugins/$PLUGIN_SLUG \
&& tar -xf - -C /var/www/html/wp-content/plugins/$PLUGIN_SLUG"
wp plugin activate "$PLUGIN_SLUG" --quiet
wp plugin list --name="$PLUGIN_SLUG" --field=status | grep -qx active \
|| { echo "The plugin did not stay active after activation." >&2; exit 1; }
echo " activated $(wp plugin get "$PLUGIN_SLUG" --field=version)"
# 🔴 The output is captured and the summary checked HERE, by the caller.
#
# A smoke file cannot assert about its own early exit: `wp eval-file` runs it
# in function scope, so a `return` partway through skips everything after it —
# including any floor the file carries at the bottom. Measured: truncating
# tests/smoke.php before the shipping section ran two thirds of the assertions
# and this script reported green, because the file's own floor was one of the
# things never reached.
#
# So the number the file prints is verified against MIN_SITE_ASSERTIONS out
# here, where an early return cannot reach. The floor inside the file stays as
# well; it catches a short run that still gets to the end.
for f in ${SITE_TESTS:-}; do
ci_step "$f"
out=$(wp eval-file "wp-content/plugins/$PLUGIN_SLUG/$f" 2>&1) || { printf '%s\n' "$out"; exit 1; }
printf '%s\n' "$out"
case "$f" in
*smoke*)
ran=$(printf '%s' "$out" | sed -n -E 's/^All ([0-9]+) assertions passed.*/\1/p' | tail -1)
if [ -z "$ran" ]; then
echo
ci_fail "$f printed no assertion total. It exited before its summary — which is what a file that returned halfway through looks like, and it cannot report that about itself."
exit 1
fi
if [ "$ran" -lt "$MIN_SITE_ASSERTIONS" ]; then
echo
ci_fail "$f ran only $ran assertions, expected at least $MIN_SITE_ASSERTIONS. Part of the suite did not run."
exit 1
fi
echo " $ran assertions, at or above the floor of $MIN_SITE_ASSERTIONS"
;;
esac
done
ci_step "PHP notices and deprecations"
# Only lines naming this plugin. WordPress core and WooCommerce deprecate their
# own things on every release, and failing on those would make the build red
# for something nobody here can fix — which is how a red build gets ignored.
log=$(wp eval 'echo WP_CONTENT_DIR;')/debug.log
if docker exec "$id-cli" test -f "$log"; then
if docker exec "$id-cli" grep -F "/plugins/$PLUGIN_SLUG/" "$log"; then
echo
ci_fail "The plugin logged the notices above on WordPress $(wp core version). They are the shape an update breaks in: harmless now, fatal two releases later."
exit 1
fi
echo " none from this plugin"
else
echo " nothing logged at all"
fi
# Set only here, so the leak check above is advisory on a run that failed for
# some other reason and fatal on one that otherwise passed.
site_test_ok=1
ci_step "done"
echo "WordPress $(wp core version)$( [ "$NEEDS_WOOCOMMERCE" = yes ] && echo " + WooCommerce $(wp plugin get woocommerce --field=version)" ) on PHP $PHP_VERSION: green"