#!/usr/bin/env bash # Build the installable plugin zip and check it is actually installable. # # Writes build/.zip, containing a single top-level / 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/.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"