2026-09-16 10:34:38 +02:00
# Shared helpers for the ci/ scripts. Sourced, never executed.
#
# Nothing in here is specific to one plugin — ci/plugin.conf holds that.
# shellcheck shell=bash
ci_repo_root() {
cd " $( dirname " ${ BASH_SOURCE [0] } " ) /.." && pwd
}
# Load ci/plugin.conf and fail loudly on anything it forgot to set. A missing
# value would otherwise turn into an empty path and a test that passes by
# testing nothing.
ci_load_conf() {
local root
root = " $( ci_repo_root) "
# shellcheck source=/dev/null
. " $root /ci/plugin.conf"
2026-09-17 00:14:40 +02:00
# MIN_* included on purpose: a port that copies this file but forgets them
# leaves the floors empty, every comparison becomes a no-op, and the hollow
# checks these were added to fix come back silently — the same shape of bug.
2026-09-16 10:34:38 +02:00
local missing =
2026-09-20 22:39:19 +02:00
for var in PLUGIN_SLUG PLUGIN_MAIN TEXT_DOMAIN NEEDS_WOOCOMMERCE MIN_SHIPPED_PHP_FILES MIN_TRANSLATION_CALLS RELEASE_REPO WPMGR_MENU_SHA OLD_PLUGIN_NAME; do
2026-09-16 10:34:38 +02:00
[ -n " ${ !var :- } " ] || missing = " $missing $var "
done
if [ -n " $missing " ] ; then
echo "ci/plugin.conf is missing: $missing " >& 2
return 1
fi
if [ ! -f " $root / $PLUGIN_MAIN " ] ; then
echo "ci/plugin.conf points PLUGIN_MAIN at ' $PLUGIN_MAIN ', which does not exist." >& 2
return 1
fi
}
# The version WordPress will compare against the update feed. Read from the
# plugin header, which is the only copy WordPress ever looks at.
ci_plugin_version() {
sed -n -E 's/^[[:space:]]*\*?[[:space:]]*Version:[[:space:]]*([^[:space:]]+).*/\1/p' \
" $( ci_repo_root) / $PLUGIN_MAIN " | head -1
}
# Read any other plugin header field, e.g. ci_plugin_header 'Requires at least'.
ci_plugin_header() {
sed -n -E "s/^[[:space:]]*\*?[[:space:]]* $1 :[[:space:]]*(.*)/\1/p" \
" $( ci_repo_root) / $PLUGIN_MAIN " | head -1 | sed -E 's/[[:space:]]+$//'
}
2026-09-16 13:18:22 +02:00
# Run a shell script against a checkout of this repository, inside a container.
#
# The repository goes in over a tarball and `docker cp`, never a bind mount: a
# Gitea Actions job runs inside a container talking to the HOST's docker
# daemon, so a host path here lands on an empty directory nobody created and
# the check passes by checking nothing. The symptom reads like a typo in a path.
#
# Two details are load-bearing. The script is copied in as a FILE rather than
# interpolated into `sh -c`, so it can contain $, quotes and newlines without
# being mangled — which matters the moment it is more than one command. And
# --entrypoint sh is not decoration: some images (composer:2, for one) have an
# entrypoint that swallows its arguments.
#
# Taken from the subscriptions port, which needed it for composer.
ci_in_container() { # image script-text
local image = " $1 " script = " $2 "
local dir cid code = 0
dir = $( mktemp -d -t wpci-XXXXXX)
( cd " $( ci_repo_root) " && git ls-files -z | tar --null -T - -cf " $dir /src.tar" )
printf '%s\n' " $script " > " $dir /run.sh"
cid = $( docker create --entrypoint sh -w /src " $image " \
-c 'mkdir -p /src && tar -xf /tmp/src.tar -C /src && exec sh /tmp/run.sh' )
docker cp " $dir /src.tar" " $cid :/tmp/src.tar" >/dev/null
docker cp " $dir /run.sh" " $cid :/tmp/run.sh" >/dev/null
docker start -a " $cid " || code = $?
docker rm -f " $cid " >/dev/null
rm -rf " $dir "
return " $code "
}
2026-09-16 10:40:19 +02:00
# Run one of this repository's PHP scripts.
#
# Uses the local php when there is one — a laptop, GitHub's hosted runners —
# and a container otherwise. The Gitea runner image has no php, and losing a
# build to "php: command not found" after everything else passed is a silly way
# to spend an afternoon. Measured: it happened on the very first Gitea run.
ci_php() {
if command -v php >/dev/null 2>& 1; then
php " $@ "
return
fi
2026-09-16 13:18:22 +02:00
ci_in_container "php:8.4-cli" "exec php $( printf '%q ' " $@ " ) "
2026-09-16 10:40:19 +02:00
}
2026-09-17 00:37:30 +02:00
# Run one of this repository's python scripts, with PyYAML available.
#
# Same shape and the same reason as ci_php above, and it is not hypothetical:
# the Gitea runner image has NO PyYAML (measured — `import yaml` raises
# ModuleNotFoundError there), while every laptop and GitHub's hosted runners
# have it. So a check that merely skips when the import fails is a check that
# is switched off on exactly one forge — and Gitea is the forge where a
# workflow that does not parse produces no run and no error to click on.
#
# alpine + py3-yaml rather than a python image + pip: no network install, one
# apk call, the same pinned answer every time.
ci_python_yaml() {
if python3 -c 'import yaml' 2>/dev/null; then
python3 " $@ "
return
fi
ci_in_container "alpine:3.20" \
"apk add --no-cache python3 py3-yaml >/dev/null && exec python3 $( printf '%q ' " $@ " ) "
}
2026-09-16 10:34:38 +02:00
ci_step() { printf '\n\033[1m→ %s\033[0m\n' " $* " ; }
ci_fail() { printf '\033[31mFAIL\033[0m %s\n' " $* " >& 2; }
# GitHub and Gitea both render these as annotations on the file and line.
ci_annotate() { # level file line message
printf '::%s file=%s,line=%s::%s\n' " $1 " " $2 " " $3 " " $4 "
}