Three things, all the same shape: a check that recognises a spelling has one blind spot per spelling, and one that reads the thing itself has none of that kind. Security hit it when an assertion matching the literal '∞' went red over a file drawing exactly that character through a CSS escape. Swept this repo's assertions for the same and found two. "the plugin itself loads the shared menu file" searched THIS FILE'S SOURCE TEXT for "'includes/shared-menu.php',". A comment mentioning that path would have satisfied it — the same hollow shape as grepping a file for the words of the guard it is supposed to have, which this kit already had once today. The admin list is now a method, admin_includes(), and the test reads the array the plugin actually loads from. Proven red by removing the entry: the failure prints the list it did find. The package-URL check asserted the absence of one particular IP address. Gitea's ROOT_URL has been a raw address before and may be again, so naming today's is blind to tomorrow's. It now asserts the host and the scheme. Proven red with the realistic fault rather than a contrived one — the raw address in the Update URI header: FAIL so it is never the raw address … expected 'gitea.nolimit.dk', got '57.129.89.117' FAIL and it is fetched over https … expected 'https', got 'http' And ci/build-zip.sh left the archive behind when it failed. Every exit in the checks section happens after the archive is written, so a failed build kept a half-built artefact that looks exactly like a good one — and both the release step and anyone publishing by hand take build/<slug>.zip by name. Found by stubbing ci/dist-files.sh empty: exit 1, correct message, zip still on disk. A trap removes it now, proven both ways. Checked while here, from Security's report that the kit's shipped-file floor was unguarded: this copy is guarded, and red for the right reason. Stubbing the file list empty gives exit 1, one FAIL naming the empty list, and zero occurrences of the "every one guarded" line — which is the assertion that matters, since being red for an unrelated reason would look identical from the exit code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
3.4 KiB
Bash
Executable File
88 lines
3.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Build the installable plugin zip and check it is actually installable.
|
|
#
|
|
# Writes build/<slug>.zip, containing a single top-level <slug>/ directory —
|
|
# WordPress identifies an installed plugin by that folder name, so a zip that
|
|
# unpacks to anything else installs a second copy alongside the first instead
|
|
# of updating it.
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/.."
|
|
. ci/lib.sh
|
|
ci_load_conf
|
|
|
|
version=$(ci_plugin_version)
|
|
[ -n "$version" ] || { echo "Could not read Version: from $PLUGIN_MAIN." >&2; exit 1; }
|
|
|
|
rm -rf build
|
|
mkdir -p "build/$PLUGIN_SLUG"
|
|
|
|
# One list, shared with the hygiene checks and the test site, so a file cannot
|
|
# be excluded from the tests and included in the release.
|
|
./ci/dist-files.sh | while IFS= read -r f; do
|
|
mkdir -p "build/$PLUGIN_SLUG/$(dirname "$f")"
|
|
cp "$f" "build/$PLUGIN_SLUG/$f"
|
|
done
|
|
|
|
# `zip` is on GitHub's runners but not in every Gitea runner image, and a
|
|
# release that fails on the packaging step is the most annoying possible place
|
|
# to discover a missing tool. python3 is present in both.
|
|
(
|
|
cd build
|
|
if command -v zip >/dev/null 2>&1; then
|
|
zip -rq "$PLUGIN_SLUG.zip" "$PLUGIN_SLUG"
|
|
elif command -v python3 >/dev/null 2>&1; then
|
|
python3 -m zipfile -c "$PLUGIN_SLUG.zip" "$PLUGIN_SLUG"
|
|
else
|
|
echo "Neither zip nor python3 is available to build the archive." >&2
|
|
exit 1
|
|
fi
|
|
)
|
|
|
|
# Listing entries by name rather than parsing `unzip -l` output, with the same
|
|
# python3 fallback as above because unzip is not guaranteed on a runner either.
|
|
zip_entries() {
|
|
if command -v unzip >/dev/null 2>&1; then
|
|
unzip -Z1 "build/$PLUGIN_SLUG.zip"
|
|
else
|
|
python3 -c 'import sys,zipfile;print("\n".join(zipfile.ZipFile(sys.argv[1]).namelist()))' \
|
|
"build/$PLUGIN_SLUG.zip"
|
|
fi
|
|
}
|
|
|
|
ci_step "what the zip contains"
|
|
zip_entries | sed 's/^/ /'
|
|
|
|
# 🔴 A failed build must not leave a zip behind. Every exit below this point
|
|
# happens AFTER the archive is written, so without this the tree keeps a
|
|
# half-built artefact that looks exactly like a good one — and the release step
|
|
# takes `build/<slug>.zip` by name, as does anyone publishing by hand. The
|
|
# check that caught it: stubbing ci/dist-files.sh empty makes this script exit
|
|
# 1 with "The main plugin file is not in the zip", and the zip was still there
|
|
# afterwards.
|
|
trap 'code=$?; [ "$code" = 0 ] || { rm -f "build/$PLUGIN_SLUG.zip"; echo "Removed the incomplete build/$PLUGIN_SLUG.zip." >&2; }; exit $code' EXIT
|
|
|
|
ci_step "checks"
|
|
zip_entries | grep -qx "$PLUGIN_SLUG/$PLUGIN_MAIN" \
|
|
|| { echo "The main plugin file is not in the zip." >&2; exit 1; }
|
|
echo " the main file is present"
|
|
|
|
# A leaked ci/ or tests/ directory is not merely untidy: everything under
|
|
# wp-content/plugins/ can in principle be fetched over HTTP from a live shop.
|
|
for unwanted in ci tests docs .git .github .gitea; do
|
|
if zip_entries | grep -q "^$PLUGIN_SLUG/$unwanted/"; then
|
|
echo "The zip contains $unwanted/, which must never reach a shop." >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
echo " no tooling, tests or git files leaked in"
|
|
|
|
# Every entry sits under exactly one top-level directory named for the slug.
|
|
strays=$(zip_entries | grep -v "^$PLUGIN_SLUG/" || true)
|
|
[ -z "$strays" ] || { echo "Files outside $PLUGIN_SLUG/ in the zip:" >&2; echo "$strays" >&2; exit 1; }
|
|
echo " everything unpacks into $PLUGIN_SLUG/"
|
|
|
|
size=$(du -h "build/$PLUGIN_SLUG.zip" | cut -f1)
|
|
echo
|
|
echo "build/$PLUGIN_SLUG.zip — version $version, $size"
|