Build the download address from the plugin header, not from the forge

The v1.4.0 release went out and the feed came back readable, with this in it:

  "browser_download_url":
    "http://57.129.89.117:25001/kw/woocommerce-email-discount-manager/..."

That field is filled from the forge's own configured root URL, which is not
necessarily the address the outside world reaches it on. A shop handed that
would be downloading plugin code over plain HTTP from a raw IP on a
non-standard port, bypassing the Cloudflare tunnel in front of gitea.nolimit.dk
and breaking the day that address changes.

parse_release() now rebuilds both the package and the release-page address from
the repository address in the plugin header, which is the one place that
already had to be right for any of this to work. tests/smoke.php pins it with
the exact response the live feed returned.

The release check had the same bug from the other end, and a worse version of
it: it verified the address the API reported rather than the one the plugin
builds, so it was a green check on a URL no shop ever uses. It now downloads
the package the plugin would download and opens the zip to confirm it is the
plugin and not an error page with a 200 on it.

And it only runs on the forge named in "Update URI". The other one is a mirror;
requiring a mirror to be anonymously readable failed the GitHub half of the
v1.4.0 release for a repository that is private on purpose.

ci/update-smoke.sh is the check none of the above can stand in for: install the
previous version into a throwaway shop and let WordPress find, download and
install the new one by itself. Every piece can be individually correct and the
update still not arrive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kristianwind
2026-09-16 10:49:52 +02:00
co-authored by Claude Opus 5
parent daa72f8f32
commit 8d0fa8c9d2
4 changed files with 231 additions and 31 deletions
+43 -7
View File
@@ -113,8 +113,25 @@ print(json.dumps({
fi
ci_step "the feed, read the way a shop reads it"
# Deliberately no token on any request below. A "check" that sends credentials
# the shops do not have is the false positive that hides exactly this bug.
# Only the forge named in "Update URI" actually serves shops. This script runs
# on both, and the other one is a mirror — requiring a mirror to be anonymously
# readable would fail every release for a repository that is private on purpose.
# (It did, on the first v1.4.0 run: GitHub is private and is not the feed.)
update_uri=$(sed -n -E 's/^[[:space:]]*\*[[:space:]]*Update URI:[[:space:]]*(.*)/\1/p' "$PLUGIN_MAIN" | head -1 | tr -d '[:space:]')
feed_host=$(printf '%s' "$update_uri" | sed -E 's|^[a-z]+://([^/]+).*|\1|')
this_host=$(printf '%s' "${GITHUB_SERVER_URL:-}" | sed -E 's|^[a-z]+://([^/]+).*|\1|')
if [ -z "$update_uri" ]; then
echo "The plugin header has no Update URI, so there is no feed to check."
exit 0
fi
if [ "$feed_host" != "$this_host" ]; then
echo " $this_host is a mirror; shops read $feed_host. Nothing to verify here."
exit 0
fi
feed=$(curl -fsS --max-time 30 "$release_api" 2>/dev/null || true)
if [ -z "$feed" ]; then
@@ -126,16 +143,35 @@ if [ -z "$feed" ]; then
exit 1
fi
package=$(printf '%s' "$feed" | tr ',' '\n' | sed -n -E 's/.*"browser_download_url":[[:space:]]*"([^"]*\.zip)".*/\1/p' | head -1)
if [ -z "$package" ]; then
printf '%s' "$feed" | grep -q '"browser_download_url"' || {
echo
ci_fail "The release is readable, but it has no zip attached. WordPress would be offered an update it cannot download."
exit 1
fi
}
code=$(curl -s -o /dev/null -w '%{http_code}' -L --max-time 60 "$package")
# Checked at the address the PLUGIN builds, not the one the API reports.
#
# A forge fills browser_download_url from its own configured root URL, which is
# not necessarily the address the outside world reaches it on: gitea.nolimit.dk
# returns http://57.129.89.117:25001/... there, a plain-HTTP raw IP that
# bypasses the Cloudflare tunnel. Verifying that address would be verifying a
# URL no shop ever uses — a green check on the wrong thing, which is worse than
# no check.
package="$update_uri/releases/download/$TAG/$PLUGIN_SLUG.zip"
code=$(curl -s -o /tmp/wpci-feed.zip -w '%{http_code}' -L --max-time 120 "$package")
echo " $package -> HTTP $code"
[ "$code" = 200 ] || { ci_fail "The package URL is not anonymously downloadable."; exit 1; }
[ "$code" = 200 ] || { ci_fail "The package URL the plugin will use is not anonymously downloadable."; exit 1; }
# And it has to be the plugin, not an error page with a 200 on it.
python3 -c '
import sys, zipfile
names = zipfile.ZipFile("/tmp/wpci-feed.zip").namelist()
main = sys.argv[1] + "/" + sys.argv[2]
if main not in names:
sys.exit("The downloaded zip does not contain " + main)
print(" it is the plugin: %d entries, %s present" % (len(names), main))
' "$PLUGIN_SLUG" "$PLUGIN_MAIN"
rm -f /tmp/wpci-feed.zip
echo
echo "Published $TAG. A shop running this plugin will be offered it within twelve hours,"