Files
nolimit-discounts/ci/site-test.sh
T
kristianwindandClaude Opus 5 d64d0fd1a8 Stop ci/site-test.sh leaking a MariaDB volume on every run
`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.

What hid it: cleanup() removed the containers, the network and the NAMED $id-wp
volume correctly. Three of the four things the run creates. Removing three of
four is indistinguishable from removing all four unless you count, which is the
same shape as every other bug found in this kit today.

Found by the subscriptions session, which traced the Gitea runner's disk being
100% full — 26 GB in 95 volumes with 1 in use — and by woo-cache-manager from
the other end, whose push failed with `unable to create temporary object
directory`, three layers away from the cause. Measured here rather than taken
on report: a mariadb:lts container creates exactly one anonymous volume, it
survives `docker rm -f`, and it is removed by `docker rm -f -v`. This machine
was holding 133 volumes, 127 dangling, 15.2 GB reclaimable.

The fix is the -v. The rest is so it cannot come back silently:

cleanup() now asks the containers which volumes they own, via docker inspect,
BEFORE removing them, and checks afterwards that none survive. Attribution
rather than a global before/after count on purpose — several sessions share
this box and run these container pairs concurrently, so a neighbour's volume
would read as this run's leak.

The check is advisory on a run that already failed, and fatal on one that
otherwise passed: site_test_ok is set at the single point the run is known to
have succeeded. And it calls exit rather than return, because `return` from an
EXIT trap does not change the script's exit status — the warning would have
printed while the build went green, which is the failure mode this whole day
has been about.

Verified: the real site test now runs green with a volume delta of zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 00:47:01 +02:00

192 lines
8.6 KiB
Bash
Executable File

#!/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}"
id="wpci-$$"
net="$id-net"
# 🔴 -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)"
for f in ${SITE_TESTS:-}; do
ci_step "$f"
wp eval-file "wp-content/plugins/$PLUGIN_SLUG/$f"
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"