diff --git a/ci/selftest.sh b/ci/selftest.sh index b514a63..334fada 100755 --- a/ci/selftest.sh +++ b/ci/selftest.sh @@ -132,31 +132,47 @@ echo "$out" | grep -qE '^ [0-9]+ shipped PHP files examined' \ || bad "the direct-access check printed no count — it cannot tell 13 files from 0" # ---------------------------------------------------------------- text domain -# No docker, so these are cheap. Three zeros, three verdicts. +# 🔴 ci_php, never a bare `php`. The Gitea runner image has NO php — that is +# why ci_php exists and says so — and this section originally called `php` +# directly. Two of its four assertions then failed there, and the other two +# PASSED FOR THE WRONG REASON: they expect a non-zero exit, and `php: command +# not found` is a non-zero exit. A green tick for a check that never ran is the +# exact thing this whole script exists to prevent, reproduced inside it. +# +# ci_php falls back to a container whose filesystem comes from `git ls-files`, +# so the fixtures live INSIDE this copy of the repo and are staged. A fixture +# outside the repo, or an unstaged one, is simply not there when it matters. cd "$work/repo" echo echo "→ ci/check-textdomain.php verdicts" -mkdir -p "$work/empty" "$work/nostrings" -printf ' "$work/nostrings/plugin.php" -php ci/check-textdomain.php --min=0 "$TEXT_DOMAIN" "$work/empty" >/dev/null 2>&1 \ +# Not a literally empty directory: git cannot track one, so it would not reach +# the container at all and the check would fail for a different reason than the +# one being asserted. +mkdir -p selftest-empty selftest-nostrings selftest-dotted/includes +printf 'no php in here\n' > selftest-empty/.keep +printf ' selftest-nostrings/plugin.php +printf " selftest-dotted/includes/class-db.php +git add -N selftest-empty/.keep selftest-nostrings/plugin.php \ + selftest-dotted/includes/class-db.php >/dev/null + +ci_php ci/check-textdomain.php --min=0 "$TEXT_DOMAIN" selftest-empty >/dev/null 2>&1 \ && bad "an empty scan root passed — the root verdict must not be configurable away" \ || ok "an empty scan root is fatal even with --min=0" -php ci/check-textdomain.php --min=999999 "$TEXT_DOMAIN" . >/dev/null 2>&1 \ +ci_php ci/check-textdomain.php --min=999999 "$TEXT_DOMAIN" . >/dev/null 2>&1 \ && bad "a --min above the real count passed" \ || ok "a --min above the real count is fatal" -php ci/check-textdomain.php "$TEXT_DOMAIN" "$work/nostrings" >/dev/null 2>&1 \ +ci_php ci/check-textdomain.php "$TEXT_DOMAIN" selftest-nostrings >/dev/null 2>&1 \ && ok "zero calls without --min warns rather than failing" \ || bad "zero calls without --min failed — a small plugin may legitimately have none" # The annotated path must be a file that exists. str_replace( $root, '', $path ) -# with the default root of "." strips every dot and yields "class-dbphp". -mkdir -p "$work/dotted/includes" -printf " "$work/dotted/includes/class-db.php" -ann=$(cd "$work/dotted" && php "$work/repo/ci/check-textdomain.php" "$TEXT_DOMAIN" . 2>&1 | sed -n 's/.*file=\([^,]*\),.*/\1/p' | head -1) -[ -n "$ann" ] && [ -f "$work/dotted/$ann" ] \ +# with a root of "." strips every dot and yields "class-dbphp". +ann=$(ci_php ci/check-textdomain.php "$TEXT_DOMAIN" selftest-dotted 2>&1 \ + | sed -n 's/.*file=\([^,]*\),.*/\1/p' | head -1) +[ -n "$ann" ] && [ -f "selftest-dotted/$ann" ] \ && ok "the annotated path exists on disk: $ann" \ || bad "the annotated path is not a file: '$ann'" @@ -225,6 +241,46 @@ ci_python_yaml ci/check-workflows.py >/dev/null 2>&1 \ && bad "check-workflows.py passed having parsed no file at all" \ || ok "parsing zero workflow files is fatal" +# ------------------------------------------------- no bare interpreter calls +# The bug that cost a green Gitea build: this script called `php` directly. +# The Gitea runner image has no php and no PyYAML, so a bare call there is +# "command not found" — which is a NON-ZERO exit, and therefore indistinguishable +# from the failure two of these assertions were written to expect. They went +# green having run nothing. +# +# ci_php and ci_python_yaml exist for this and say so in their comments. lib.sh +# is excluded because it is where the real calls live. +# +# Measured, so the rule is not uniform: the runner HAS python3 and has NO php, +# and PyYAML is the only missing module. A bare python3 over the stdlib is +# therefore correct and is not flagged. +echo +echo "→ no ci/ script may call php or python3 directly" +# Narrow on purpose. A broad "the word php appears" fires on every string +# literal that mentions php, and on the calls INSIDE the docker scripts in +# static.sh and site-test.sh, where php is exactly right because the container +# is a php image. A check that cries wolf gets commented out, which is worse +# than not having it. These two shapes are what the real bug looked like: the +# interpreter as the first word of a command, or inside $( ). +bare=$(cd "$work/repo" && for f in ci/*.sh; do + [ "$f" = ci/lib.sh ] && continue + sed 's/#.*//' "$f" \ + | grep -nE '^[[:space:]]*php[[:space:]]|\$\((cd [^)]*&&[[:space:]]*)?php[[:space:]]' \ + | sed "s|^|$f:|" + # python3 itself IS on the Gitea runner; only PyYAML is missing. So a bare + # python3 is fine for anything stdlib (build-zip.sh's zipfile, the JSON + # readers in publish-release.sh) and wrong only when yaml is involved. + sed 's/#.*//' "$f" \ + | grep -nE '^[[:space:]]*python3[[:space:]].*(yaml|check-workflows)' \ + | sed "s|^|$f:|" +done) +if [ -n "$bare" ]; then + printf '%s\n' "$bare" | sed 's/^/ /' + bad "a ci/ script calls an interpreter directly instead of ci_php / ci_python_yaml" +else + ok "every interpreter call goes through the container-capable wrapper" +fi + echo if [ "$fail" != 0 ]; then echo "Self-test: $fail of $((pass + fail)) assertions FAILED."