143 lines
5.6 KiB
Bash
143 lines
5.6 KiB
Bash
#!/usr/bin/env bash
|
|||
|
|
# Publish a release with the plugin zip attached, on whichever forge is running
|
||
|
|
# this, and then verify the result the way a shop will actually see it.
|
||
|
|
#
|
||
|
|
# Needs: TAG, GITHUB_REPOSITORY, GITHUB_API_URL, GITHUB_SERVER_URL, GH_TOKEN.
|
||
|
|
#
|
||
|
|
# The verification at the end is the part worth keeping. The update feed is
|
||
|
|
# read anonymously by every shop, and the two ways it breaks — a private
|
||
|
|
# repository and a release with no zip attached — both leave a release page
|
||
|
|
# that looks perfectly fine to whoever is logged in. Nobody would notice for
|
||
|
|
# months; what they would notice is that updates stopped arriving.
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
cd "$(dirname "$0")/.."
|
||
|
|
. ci/lib.sh
|
||
|
|
ci_load_conf
|
||
|
|
|
||
|
|
: "${TAG:?TAG is not set}"
|
||
|
|
: "${GH_TOKEN:?GH_TOKEN is not set}"
|
||
|
|
: "${GITHUB_REPOSITORY:?}"
|
||
|
|
: "${GITHUB_API_URL:?}"
|
||
|
|
|
||
|
|
zip="build/$PLUGIN_SLUG.zip"
|
||
|
|
[ -f "$zip" ] || { echo "$zip does not exist — run ci/build-zip.sh first." >&2; exit 1; }
|
||
|
|
|
||
|
|
owner="${GITHUB_REPOSITORY%%/*}"
|
||
|
|
repo="${GITHUB_REPOSITORY#*/}"
|
||
|
|
|
||
|
|
# The tag message is the announcement, so it has to survive the checkout.
|
||
|
|
#
|
||
|
|
# actions/checkout fetches every tag correctly and then overwrites this one ref
|
||
|
|
# with the commit SHA — `fetch-depth: 0` does not save you. The annotated tag
|
||
|
|
# becomes a lightweight one pointing at the commit, and reading its message
|
||
|
|
# returns the COMMIT message instead: on a merge commit, "Merge pull request
|
||
|
|
# #12 from ...". Nothing fails. The release is simply published with the wrong
|
||
|
|
# text, and only the page shows it.
|
||
|
|
git fetch --force origin "refs/tags/$TAG:refs/tags/$TAG"
|
||
|
|
|
||
|
|
[ "$(git cat-file -t "$TAG")" = tag ] || {
|
||
|
|
echo "$TAG is not an annotated tag. Its message is what gets published, so create it with 'git tag -a'." >&2
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
notes=$(git tag -l --format='%(contents)' "$TAG")
|
||
|
|
first_line=$(printf '%s\n' "$notes" | head -1)
|
||
|
|
|
||
|
|
case "$first_line" in
|
||
|
|
'Merge pull request'*|'Merge branch'*|'')
|
||
|
|
echo "The release notes read '$first_line'." >&2
|
||
|
|
echo "That is a commit message, not the tag message — the annotated tag did not survive the checkout." >&2
|
||
|
|
exit 1
|
||
|
|
;;
|
||
|
|
esac
|
||
|
|
|
||
|
|
ci_step "release notes"
|
||
|
|
printf '%s\n' "$notes" | sed 's/^/ /'
|
||
|
|
|
||
|
|
# The updater reads this line out of the release body, so wp-admin can show
|
||
|
|
# what this version was tested against rather than what the last one claimed.
|
||
|
|
readme=
|
||
|
|
for f in readme.txt README.txt; do [ -f "$f" ] && readme=$f && break; done
|
||
|
|
tested_wp=$([ -n "$readme" ] && sed -n -E 's/^Tested up to:[[:space:]]*([^[:space:]]+).*/\1/p' "$readme" | head -1)
|
||
|
|
tested_wc=$(ci_plugin_header 'WC tested up to')
|
||
|
|
|
||
|
|
body=$(printf '%s\n\n---\n\nTested up to: %s\nWC tested up to: %s\n' \
|
||
|
|
"$notes" "${tested_wp:-unknown}" "${tested_wc:-unknown}")
|
||
|
|
|
||
|
|
api() { # method path [data]
|
||
|
|
curl -fsS -X "$1" \
|
||
|
|
-H "Authorization: token $GH_TOKEN" \
|
||
|
|
-H 'Accept: application/json' \
|
||
|
|
-H 'Content-Type: application/json' \
|
||
|
|
${3:+-d "$3"} \
|
||
|
|
"$2"
|
||
|
|
}
|
||
|
|
|
||
|
|
if [ "${GITHUB_SERVER_URL:-}" = 'https://github.com' ]; then
|
||
|
|
ci_step "publishing to GitHub"
|
||
|
|
gh release create "$TAG" "$zip" --title "$TAG" --notes "$body" 2>/dev/null \
|
||
|
|
|| gh release upload "$TAG" "$zip" --clobber
|
||
|
|
release_api="$GITHUB_API_URL/repos/$owner/$repo/releases/tags/$TAG"
|
||
|
|
else
|
||
|
|
ci_step "publishing to $GITHUB_SERVER_URL"
|
||
|
|
payload=$(printf '%s' "$body" | python3 -c '
|
||
|
|
import json, sys, os
|
||
|
|
print(json.dumps({
|
||
|
|
"tag_name": os.environ["TAG"],
|
||
|
|
"name": os.environ["TAG"],
|
||
|
|
"body": sys.stdin.read(),
|
||
|
|
"draft": False,
|
||
|
|
"prerelease": False,
|
||
|
|
}))')
|
||
|
|
|
||
|
|
# A re-run of a release must not fail on "already exists".
|
||
|
|
existing=$(curl -fsS -H "Authorization: token $GH_TOKEN" \
|
||
|
|
"$GITHUB_API_URL/repos/$owner/$repo/releases/tags/$TAG" 2>/dev/null || true)
|
||
|
|
id=$(printf '%s' "$existing" | sed -n -E 's/.*"id":[[:space:]]*([0-9]+).*/\1/p' | head -1)
|
||
|
|
|
||
|
|
if [ -z "$id" ]; then
|
||
|
|
created=$(api POST "$GITHUB_API_URL/repos/$owner/$repo/releases" "$payload")
|
||
|
|
id=$(printf '%s' "$created" | sed -n -E 's/.*"id":[[:space:]]*([0-9]+).*/\1/p' | head -1)
|
||
|
|
fi
|
||
|
|
[ -n "$id" ] || { echo "Could not create or find the release for $TAG." >&2; exit 1; }
|
||
|
|
echo " release id $id"
|
||
|
|
|
||
|
|
curl -fsS -X POST \
|
||
|
|
-H "Authorization: token $GH_TOKEN" \
|
||
|
|
-F "attachment=@$zip" \
|
||
|
|
"$GITHUB_API_URL/repos/$owner/$repo/releases/$id/assets?name=$PLUGIN_SLUG.zip" >/dev/null
|
||
|
|
echo " attached $PLUGIN_SLUG.zip"
|
||
|
|
|
||
|
|
release_api="$GITHUB_API_URL/repos/$owner/$repo/releases/latest"
|
||
|
|
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.
|
||
|
|
feed=$(curl -fsS --max-time 30 "$release_api" 2>/dev/null || true)
|
||
|
|
|
||
|
|
if [ -z "$feed" ]; then
|
||
|
|
echo
|
||
|
|
ci_fail "$release_api cannot be read without credentials, so no shop will ever be offered this update."
|
||
|
|
echo "Either the repository is still private, or the release was not created."
|
||
|
|
echo "For Gitea: Settings → make the repository public. The update feed is a public read by design;"
|
||
|
|
echo "that is the whole mechanism, and there is nothing secret in a GPL plugin zip anyway."
|
||
|
|
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
|
||
|
|
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")
|
||
|
|
echo " $package -> HTTP $code"
|
||
|
|
[ "$code" = 200 ] || { ci_fail "The package URL is not anonymously downloadable."; exit 1; }
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo "Published $TAG. A shop running this plugin will be offered it within twelve hours,"
|
||
|
|
echo "or immediately on Dashboard → Updates → Check again."
|