Four checks in ci/ printed a reassuring line having examined nothing. Found independently by the woo-ai-manager and woo-cache-manager sessions in their own copies of this kit; all four reproduce here. A check that examined nothing prints the same sentence as one that examined everything, and only the passing side can tell them apart. That is why these survived being verified: a pattern that is too broad fails loudly the first time it meets real code, while one that finds nothing passes forever. So every success line now carries a count — the old text domain line could not tell 142 from 0. - static.sh, direct-access guard: counts what the loop examined and fails below MIN_SHIPPED_PHP_FILES. shipped_files() swallows a dist-files.sh failure with `|| true`, so an empty list is one broken .distignore away, and this is the check standing between a PHP file and being requested straight from a browser outside every capability check the code assumes has happened. The floor is above 1 because a pattern that wrongly excludes includes/ collapses the list without emptying it. - static.sh, credential scan: counts what it read, fatal on zero. A pushed key is compromised even after the commit is deleted, so a silent no-op here is not a weak check but the absence of one wearing a green tick. The floor is 1 and is not configurable: this list has no filter that could go partially wrong, so the only way it misreports is by coming out empty. - check-textdomain.php: three zeros, three verdicts. No PHP files at all is always fatal and deliberately not caller-configurable — it says the scan root is wrong, not that the plugin is small. Below --min is fatal. Zero calls with no --min is a warning, since a small plugin may legitimately have no translatable strings, but it is never silent. - check-textdomain.php: fix the annotated path. str_replace( $root, '', $path ) with the default root of "." removed every dot anywhere in the path, so a real error in includes/class-db.php was reported on "includes/class-dbphp" — not a file, not clickable, and not greppable. Every error this script printed with the default root was mangled that way. The floors live in ci/plugin.conf, the only file under ci/ that knows anything about this plugin, rather than hardcoded in static.sh: static.sh is copied verbatim between plugins too, so a number in it would make this plugin's shape everybody's constraint. ci_load_conf now requires them — an unset floor turns its comparison into a no-op and silently restores the bug. Each floor was broken once and watched to fail before being believed: empty shipped list, wrong scan root, --min above the real count, zero calls without --min, empty credential list, and a plugin.conf missing both floors. Static checks green, and now report 13 shipped PHP files, 142 translation calls across 17 files, 44 tracked files read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
106 lines
3.9 KiB
Bash
106 lines
3.9 KiB
Bash
# 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"
|
|
|
|
# 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.
|
|
local missing=
|
|
for var in PLUGIN_SLUG PLUGIN_MAIN TEXT_DOMAIN NEEDS_WOOCOMMERCE MIN_SHIPPED_PHP_FILES MIN_TRANSLATION_CALLS; do
|
|
[ -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:]]+$//'
|
|
}
|
|
|
|
# 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"
|
|
}
|
|
|
|
# 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
|
|
|
|
ci_in_container "php:8.4-cli" "exec php $(printf '%q ' "$@")"
|
|
}
|
|
|
|
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"
|
|
}
|