Files
argus-wp-defence/bin/build-release.sh
T
ARGUSandClaude Sonnet 5 8a6c9ca4de ARGUS WordPress Defence 7.23.1 -- real, working automatic updates
Fixes a real bug: a normal (non-critical) update was previously
detected and shown as available, but nothing ever actually installed
it -- there was no button, cron path, or any other code that did.
Automatic updates only ever worked for updates flagged critical, which
isn't what "automatic" means. Now any newer, compatible,
signature-verified update installs on its own.

This is also the first release where the self-update mechanism itself
ships in this self-distributed channel -- it was unconditionally
excluded from every previous build (a WordPress.org-only restriction
that doesn't apply here, since this channel isn't WordPress.org).

manifest.json in this repo is the real, live update manifest: signed
with Ed25519 (public key documented in README.md's Updating section),
pointing at this exact release's ZIP and its real SHA-256. Verified
end-to-end before publishing -- not just "the code looks right": ran a
full real update cycle (an older installed version checking this
manifest, downloading this exact package, verifying its signature and
hash, replacing itself, and the site continuing to work with zero
errors afterward) using the actual signing key and the actual
package this commit ships.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 19:19:40 +00:00

128 lines
5.3 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Production release packaging -- docs/design/production-distribution-and-updates.md
# sections 1-3, 16, 18. Builds the public download ZIP from a clean git
# checkout (git archive -- never the working tree, so uncommitted local
# files can never leak into a release), strips comments/whitespace
# (not obfuscation -- see strip-comments.php's own header), and audits
# the result against an explicit allowlist before packaging, failing
# closed on anything unexpected rather than silently shipping it.
#
# Usage: bin/build-release.sh [git-ref] [channel]
# channel: "self" (default) -- the self-distributed release (Gitea
# releases page), includes the signed self-update client, since
# nothing prohibits it outside WordPress.org.
# "wporg" -- strips the self-update client too (and the
# license/trial system, stripped either way) -- required before
# any submission to the WordPress.org Plugin Directory, which
# prohibits a plugin using any update channel but its own.
#
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REF="${1:-HEAD}"
CHANNEL="${2:-self}"
BUILD_DIR="$(mktemp -d)"
STAGE_DIR="${BUILD_DIR}/argus-wordpress-defence"
OUT_DIR="${REPO_ROOT}/dist"
cleanup() { rm -rf "${BUILD_DIR}"; }
trap cleanup EXIT
echo "==> Packaging ${REF} from a clean checkout (git archive, not the working tree)"
mkdir -p "${STAGE_DIR}"
git -C "${REPO_ROOT}" archive "${REF}" | tar -x -C "${STAGE_DIR}"
echo "==> Removing dev-only paths not meant for the production artifact"
rm -rf \
"${STAGE_DIR}/docs" \
"${STAGE_DIR}/tests" \
"${STAGE_DIR}/README.md" \
"${STAGE_DIR}/ARGUS_WORDPRESS_SECURITY_ARCHITECTURE.md" \
"${STAGE_DIR}/WORDPRESS_ORG_READINESS_AUDIT.md" \
"${STAGE_DIR}/.gitignore" \
"${STAGE_DIR}/bin"
if [ "${CHANNEL}" = "wporg" ]; then
# A plugin hosted on WordPress.org must rely solely on WordPress.org's own
# update channel -- never a self-update-from-external-manifest mechanism,
# even one that ships inert-by-default. class_exists( 'Argus_Update_Client' )
# guards every reference to this class elsewhere in the codebase, so
# removing the file here is sufficient; nothing else needs to change.
echo "==> [wporg channel] Removing the self-update client (WordPress.org must be the only update channel)"
rm -f "${STAGE_DIR}/includes/class-argus-update-client.php"
elif [ "${CHANNEL}" != "self" ]; then
echo "==> BUILD FAILED: unknown channel '${CHANNEL}' (expected 'self' or 'wporg')"
exit 1
fi
# WordPress.org explicitly prohibits trialware -- a plugin submitted to the
# directory must stay genuinely, permanently functional with no license
# requirement. class_exists( 'Argus_License' ) guards every reference to
# this class elsewhere, so removing the file here is sufficient; the free
# build never has a trial timer at all.
echo "==> Removing the license/trial system (WordPress.org prohibits trialware)"
rm -f "${STAGE_DIR}/includes/class-argus-license.php"
echo "==> Allowlist audit -- fail closed on anything unexpected"
UNEXPECTED=0
while IFS= read -r -d '' item; do
rel="${item#"${STAGE_DIR}"/}"
case "$rel" in
argus-wordpress-defence.php|uninstall.php|readme.txt) ;;
admin|admin/*|includes|includes/*|mu-loader|mu-loader/*|assets|assets/*|languages|languages/*) ;;
*)
if [ -f "$item" ]; then
echo " UNEXPECTED FILE: ${rel}"
UNEXPECTED=1
fi
;;
esac
done < <(find "${STAGE_DIR}" -print0)
# Reject known-forbidden patterns explicitly, even inside allowlisted dirs.
if find "${STAGE_DIR}" \( -name "*.key" -o -name "*.pem" -o -name ".env*" -o -name "*.map" \) | grep -q .; then
echo " FORBIDDEN FILE TYPE found (key/pem/.env/source map)"
UNEXPECTED=1
fi
if [ "${UNEXPECTED}" -ne 0 ]; then
echo "==> BUILD FAILED: allowlist audit found unexpected content. Nothing was packaged."
exit 1
fi
echo " clean -- only allowlisted paths present"
# Read the version BEFORE stripping -- it lives inside the plugin
# header's own DocBlock comment, which php_strip_whitespace() below
# would otherwise remove before this ever got a chance to read it.
VERSION="$(grep -oP '(?<=Version:)\s*\K\S+' "${STAGE_DIR}/argus-wordpress-defence.php" | head -1)"
if [ -z "${VERSION}" ]; then
echo "==> BUILD FAILED: could not read plugin version from argus-wordpress-defence.php"
exit 1
fi
echo "==> Packaging version ${VERSION}"
echo "==> Stripping comments/whitespace (not obfuscation -- see bin/strip-comments.php)"
php "${REPO_ROOT}/bin/strip-comments.php" "${STAGE_DIR}"
mkdir -p "${OUT_DIR}"
ZIP_PATH="${OUT_DIR}/argus-wordpress-defence-${VERSION}.zip"
rm -f "${ZIP_PATH}"
# Prefer the `zip` CLI when present (most CI runners have it); fall
# back to PHP's ZipArchive (bin/zip-directory.php) for a host that
# only has the PHP extension -- either is a real, complete archive, no
# feature difference between the two paths.
if command -v zip >/dev/null 2>&1; then
( cd "${BUILD_DIR}" && zip -rq "${ZIP_PATH}" "argus-wordpress-defence" )
else
php "${REPO_ROOT}/bin/zip-directory.php" "${STAGE_DIR}" "${ZIP_PATH}" "argus-wordpress-defence"
fi
SHA256="$(sha256sum "${ZIP_PATH}" | cut -d' ' -f1)"
echo "${SHA256} $(basename "${ZIP_PATH}")" > "${ZIP_PATH}.sha256"
echo "==> Done"
echo " ${ZIP_PATH}"
echo " SHA-256: ${SHA256}"
echo " Size: $(du -h "${ZIP_PATH}" | cut -f1)"