79e46ccf7e
Issue 1: `unzip -l "$ZIP_PATH" | grep -q ...` combined with set -o pipefail silently fails when grep -q exits on first match and SIGPIPEs the upstream unzip (which has not yet finished streaming the long file list). pipefail sees unzip's non-zero exit and fails the pipeline, so the guard thinks the file is missing. Fix: capture unzip -l output to a variable once, then grep the string via herestring. The first two sanity checks using `unzip -p` (short output, tight race window) were not affected. Issue 2: test_wp_plugin_info_json_download_url_contains_expected_asset_name assumed Gitea asset URLs always contain the asset filename. This is not true on git.netranking.ru, which returns /attachments/<uuid> — a UUID-only URL. Relaxed the test to assert the URL is an HTTPS Gitea URL; freshness of download_url is enforced indirectly via the version-match test. Post-mortem: 1.2.0 release was completed via a manual recovery script after release.sh failed at the zip-sanity step. State on main is consistent: tag v1.2.0 -> release commit, JSON commit directly on top of release commit, JSON points to the live Gitea asset.
293 lines
10 KiB
Bash
Executable File
293 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# release.sh — Release helper for cf7-spam-to-telegram.
|
|
#
|
|
# Usage: ./release.sh <semver>
|
|
# Example: ./release.sh 1.2.0
|
|
#
|
|
# Prerequisites (validated below):
|
|
# - git, curl, jq, unzip, awk, sed in PATH
|
|
# - clean git tree on main, in sync with origin
|
|
# - ~/.gitea-token exists (chmod 600), scope: write:repository
|
|
# - readme.txt already contains `= <version> =` section with changelog body
|
|
#
|
|
# See docs/superpowers/specs/2026-04-21-cf7stg-self-hosted-updates-design.md
|
|
# for the full design rationale.
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_OWNER="bryzgalov"
|
|
REPO_NAME="cf7-spam-to-telegram"
|
|
GITEA_BASE="https://git.netranking.ru"
|
|
GITEA_API="${GITEA_BASE}/api/v1"
|
|
TOKEN_FILE="${HOME}/.gitea-token"
|
|
|
|
log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
|
|
warn() { printf '\033[1;33m==> WARN:\033[0m %s\n' "$*" >&2; }
|
|
fail() { printf '\033[1;31m==> FAIL:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
# ---------- STEP 1: VALIDATE ---------------------------------------------------
|
|
|
|
VERSION="${1:-}"
|
|
[[ -n "$VERSION" ]] || fail "usage: $0 <semver> (e.g. $0 1.2.0)"
|
|
|
|
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
|
|
|| fail "invalid semver: '$VERSION'"
|
|
|
|
for cmd in git curl jq unzip awk sed; do
|
|
command -v "$cmd" >/dev/null 2>&1 || fail "missing command in PATH: $cmd"
|
|
done
|
|
|
|
[[ -f "$TOKEN_FILE" && -r "$TOKEN_FILE" ]] \
|
|
|| fail "$TOKEN_FILE missing or unreadable (chmod 600 and create token in Gitea UI)"
|
|
TOKEN="$(cat "$TOKEN_FILE")"
|
|
[[ -n "$TOKEN" ]] || fail "$TOKEN_FILE is empty"
|
|
|
|
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
|
[[ "$BRANCH" == "main" ]] || fail "must be on main branch (currently: $BRANCH)"
|
|
|
|
if [[ -n "$(git status --porcelain)" ]]; then
|
|
fail "working tree not clean (commit/stash first):
|
|
$(git status --porcelain)"
|
|
fi
|
|
|
|
log "Fetching origin to compare main..."
|
|
git fetch origin main --quiet
|
|
LOCAL="$(git rev-parse main)"
|
|
REMOTE="$(git rev-parse origin/main)"
|
|
[[ "$LOCAL" == "$REMOTE" ]] \
|
|
|| fail "main is not in sync with origin/main (local=$LOCAL remote=$REMOTE — pull/push first)"
|
|
|
|
TAG="v${VERSION}"
|
|
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
|
fail "tag $TAG already exists locally"
|
|
fi
|
|
if git ls-remote --tags origin "$TAG" | grep -q "$TAG"; then
|
|
fail "tag $TAG already exists on origin"
|
|
fi
|
|
|
|
grep -qE "^=[[:space:]]*${VERSION}[[:space:]]*=[[:space:]]*$" readme.txt \
|
|
|| fail "readme.txt does not contain '= ${VERSION} =' changelog section — add it before running release"
|
|
|
|
log "Validation passed: version=$VERSION, tag=$TAG, branch=$BRANCH"
|
|
|
|
# ---------- STEP 2: BUMP VERSION ---------------------------------------------
|
|
|
|
log "Bumping version to $VERSION in cf7-spam-to-telegram.php and readme.txt"
|
|
|
|
sed -i.bak -E \
|
|
-e "s|^([[:space:]]*\\*[[:space:]]*Version:[[:space:]]*).*$|\\1${VERSION}|" \
|
|
-e "s|define\\('CF7STG_VERSION',[[:space:]]*'[^']+'\\)|define('CF7STG_VERSION', '${VERSION}')|" \
|
|
cf7-spam-to-telegram.php
|
|
rm cf7-spam-to-telegram.php.bak
|
|
|
|
grep -q "^ \* Version: ${VERSION}$" cf7-spam-to-telegram.php \
|
|
|| fail "bump failed: header Version in cf7-spam-to-telegram.php"
|
|
grep -q "define('CF7STG_VERSION', '${VERSION}')" cf7-spam-to-telegram.php \
|
|
|| fail "bump failed: CF7STG_VERSION constant in cf7-spam-to-telegram.php"
|
|
|
|
sed -i.bak -E "s|^(Stable tag:[[:space:]]*).*$|\\1${VERSION}|" readme.txt
|
|
rm readme.txt.bak
|
|
|
|
grep -q "^Stable tag: ${VERSION}$" readme.txt \
|
|
|| fail "bump failed: Stable tag in readme.txt"
|
|
|
|
log "Bumped to $VERSION"
|
|
|
|
# ---------- STEP 3: RELEASE COMMIT + PUSH ------------------------------------
|
|
|
|
log "Creating release commit"
|
|
git add cf7-spam-to-telegram.php readme.txt
|
|
git commit -m "release: ${VERSION}"
|
|
RELEASE_SHA="$(git rev-parse HEAD)"
|
|
log "Release commit: $RELEASE_SHA"
|
|
|
|
log "Pushing main to origin"
|
|
git push origin main
|
|
|
|
# ---------- STEP 4: BUILD ZIP + SANITY ---------------------------------------
|
|
|
|
ZIP_NAME="cf7-spam-to-telegram-${VERSION}.zip"
|
|
ZIP_PATH="dist/${ZIP_NAME}"
|
|
|
|
log "Building release zip: $ZIP_PATH"
|
|
mkdir -p dist
|
|
rm -f "$ZIP_PATH"
|
|
git archive HEAD \
|
|
--worktree-attributes \
|
|
--format=zip \
|
|
--prefix=cf7-spam-to-telegram/ \
|
|
-o "$ZIP_PATH"
|
|
|
|
log "Running zip sanity checks"
|
|
|
|
unzip -p "$ZIP_PATH" cf7-spam-to-telegram/cf7-spam-to-telegram.php \
|
|
| grep -q "^ \* Version: ${VERSION}$" \
|
|
|| fail "zip sanity: header Version mismatch inside $ZIP_PATH"
|
|
|
|
unzip -p "$ZIP_PATH" cf7-spam-to-telegram/cf7-spam-to-telegram.php \
|
|
| grep -q "CF7STG_VERSION', '${VERSION}'" \
|
|
|| fail "zip sanity: CF7STG_VERSION mismatch inside $ZIP_PATH"
|
|
|
|
# Cache zip listing once — `unzip -l | grep -q` triggers SIGPIPE to unzip with
|
|
# set -o pipefail, falsely failing the pipeline. Capture the list, then grep the string.
|
|
ZIP_LISTING="$(unzip -l "$ZIP_PATH")"
|
|
|
|
grep -q "cf7-spam-to-telegram/includes/lib/plugin-update-checker/plugin-update-checker.php" <<< "$ZIP_LISTING" \
|
|
|| fail "zip sanity: PUC loader missing from $ZIP_PATH"
|
|
|
|
grep -q "cf7-spam-to-telegram/wp-plugin-info.json" <<< "$ZIP_LISTING" \
|
|
|| fail "zip sanity: wp-plugin-info.json missing from $ZIP_PATH"
|
|
|
|
for dev in tests/ docs/ composer.json composer.lock phpunit.xml release.sh CLAUDE.md AGENTS.md .gitattributes .gitignore; do
|
|
if grep -q "cf7-spam-to-telegram/${dev}" <<< "$ZIP_LISTING"; then
|
|
fail "zip sanity: dev file must not be in zip: $dev"
|
|
fi
|
|
done
|
|
|
|
ZIP_SIZE="$(wc -c < "$ZIP_PATH")"
|
|
log "Zip built OK: $ZIP_PATH ($ZIP_SIZE bytes)"
|
|
|
|
# ---------- STEP 5: GITEA CREATE RELEASE -------------------------------------
|
|
|
|
log "Extracting changelog body for version $VERSION from readme.txt"
|
|
|
|
CHANGELOG_BODY="$(awk -v ver="$VERSION" '
|
|
BEGIN { in_block = 0 }
|
|
/^=[[:space:]]*[0-9]+\.[0-9]+\.[0-9]+[[:space:]]*=[[:space:]]*$/ {
|
|
if (in_block) exit
|
|
if ($0 ~ "^=[[:space:]]*" ver "[[:space:]]*=[[:space:]]*$") {
|
|
in_block = 1
|
|
next
|
|
}
|
|
}
|
|
in_block { print }
|
|
' readme.txt | sed '/^[[:space:]]*$/d')"
|
|
|
|
[[ -n "$CHANGELOG_BODY" ]] \
|
|
|| fail "could not extract changelog body for version $VERSION from readme.txt"
|
|
|
|
log "Creating Gitea release $TAG"
|
|
|
|
CREATE_PAYLOAD="$(jq -n \
|
|
--arg tag "$TAG" \
|
|
--arg name "$VERSION" \
|
|
--arg body "$CHANGELOG_BODY" \
|
|
--arg target "$RELEASE_SHA" \
|
|
'{tag_name:$tag, name:$name, body:$body, target_commitish:$target, draft:false, prerelease:false}')"
|
|
|
|
CREATE_RESPONSE="$(curl -fsSL \
|
|
-X POST "${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/releases" \
|
|
-H "Authorization: token ${TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$CREATE_PAYLOAD")"
|
|
|
|
RELEASE_ID="$(echo "$CREATE_RESPONSE" | jq -r '.id')"
|
|
[[ -n "$RELEASE_ID" && "$RELEASE_ID" != "null" ]] \
|
|
|| fail "Gitea create-release response missing id: $CREATE_RESPONSE"
|
|
|
|
log "Created Gitea release id=$RELEASE_ID"
|
|
|
|
# ---------- STEP 6: UPLOAD ASSET ---------------------------------------------
|
|
|
|
log "Uploading asset $ZIP_NAME to release id=$RELEASE_ID"
|
|
|
|
UPLOAD_RESPONSE="$(curl -fsSL \
|
|
-X POST "${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
|
|
-H "Authorization: token ${TOKEN}" \
|
|
-F "attachment=@${ZIP_PATH};type=application/zip")"
|
|
|
|
DOWNLOAD_URL="$(echo "$UPLOAD_RESPONSE" | jq -r '.browser_download_url')"
|
|
[[ -n "$DOWNLOAD_URL" && "$DOWNLOAD_URL" != "null" ]] \
|
|
|| fail "Gitea asset upload response missing browser_download_url: $UPLOAD_RESPONSE"
|
|
|
|
log "Asset uploaded: $DOWNLOAD_URL"
|
|
|
|
# ---------- STEP 7: REGENERATE wp-plugin-info.json ---------------------------
|
|
|
|
log "Converting readme.txt Changelog to HTML"
|
|
|
|
CHANGELOG_HTML="$(awk '
|
|
function escape(s) {
|
|
gsub(/&/, "\\&", s)
|
|
gsub(/</, "\\<", s)
|
|
gsub(/>/, "\\>", s)
|
|
return s
|
|
}
|
|
BEGIN { in_changelog = 0; ul_open = 0; html = ""; pending = "" }
|
|
/^== Changelog ==/ { in_changelog = 1; next }
|
|
/^==/ && in_changelog { exit }
|
|
!in_changelog { next }
|
|
/^=[[:space:]]*[0-9]+\.[0-9]+\.[0-9]+[[:space:]]*=[[:space:]]*$/ {
|
|
if (pending != "") { html = html "<li>" escape(pending) "</li>"; pending = "" }
|
|
if (ul_open) { html = html "</ul>"; ul_open = 0 }
|
|
match($0, /[0-9]+\.[0-9]+\.[0-9]+/)
|
|
ver = substr($0, RSTART, RLENGTH)
|
|
html = html "<h4>" ver "</h4><ul>"
|
|
ul_open = 1
|
|
next
|
|
}
|
|
/^\*[[:space:]]/ {
|
|
if (pending != "") { html = html "<li>" escape(pending) "</li>" }
|
|
sub(/^\*[[:space:]]+/, "")
|
|
pending = $0
|
|
next
|
|
}
|
|
/^[[:space:]]+[^[:space:]]/ {
|
|
if (pending != "") {
|
|
line = $0
|
|
sub(/^[[:space:]]+/, "", line)
|
|
pending = pending " " line
|
|
}
|
|
next
|
|
}
|
|
/^[[:space:]]*$/ {
|
|
if (pending != "") { html = html "<li>" escape(pending) "</li>"; pending = "" }
|
|
next
|
|
}
|
|
END {
|
|
if (pending != "") { html = html "<li>" escape(pending) "</li>" }
|
|
if (ul_open) { html = html "</ul>" }
|
|
print html
|
|
}
|
|
' readme.txt)"
|
|
|
|
[[ -n "$CHANGELOG_HTML" ]] || fail "changelog HTML conversion produced empty result"
|
|
|
|
LAST_UPDATED="$(date -u '+%Y-%m-%d %H:%M:%S')"
|
|
|
|
log "Regenerating wp-plugin-info.json"
|
|
TMP_JSON="$(mktemp)"
|
|
jq \
|
|
--arg version "$VERSION" \
|
|
--arg download_url "$DOWNLOAD_URL" \
|
|
--arg last_updated "$LAST_UPDATED" \
|
|
--arg changelog "$CHANGELOG_HTML" \
|
|
'.version = $version | .download_url = $download_url | .last_updated = $last_updated | .sections.changelog = $changelog' \
|
|
wp-plugin-info.json > "$TMP_JSON"
|
|
mv "$TMP_JSON" wp-plugin-info.json
|
|
|
|
jq empty wp-plugin-info.json || fail "regenerated wp-plugin-info.json is not valid JSON"
|
|
|
|
log "Regenerated wp-plugin-info.json (version=$VERSION, download_url=$DOWNLOAD_URL)"
|
|
|
|
# ---------- STEP 8: JSON COMMIT + PUSH ---------------------------------------
|
|
|
|
log "Committing wp-plugin-info.json update"
|
|
git add wp-plugin-info.json
|
|
git commit -m "wp-plugin-info.json: update for ${VERSION}"
|
|
git push origin main
|
|
|
|
# ---------- STEP 9: SUMMARY --------------------------------------------------
|
|
|
|
log "Release completed"
|
|
printf '\n'
|
|
printf ' Version : %s\n' "$VERSION"
|
|
printf ' Tag : %s (on %s/%s/%s/src/tag/%s)\n' "$TAG" "$GITEA_BASE" "$REPO_OWNER" "$REPO_NAME" "$TAG"
|
|
printf ' Release page : %s/%s/%s/releases/tag/%s\n' "$GITEA_BASE" "$REPO_OWNER" "$REPO_NAME" "$TAG"
|
|
printf ' Asset : %s\n' "$DOWNLOAD_URL"
|
|
printf ' PUC JSON : %s/%s/%s/raw/branch/main/wp-plugin-info.json\n' "$GITEA_BASE" "$REPO_OWNER" "$REPO_NAME"
|
|
printf '\n'
|
|
printf ' Next: in WordPress admin — «Плагины → Проверить обновления» (PUC re-checks hourly, force-check via transient delete if impatient).\n'
|
|
printf '\n'
|