feat(wpmc S7): release.sh Gitea release helper (mirror cf7stg flow)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Executable
+306
@@ -0,0 +1,306 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# release.sh — Release helper for wp-multi-city.
|
||||||
|
#
|
||||||
|
# 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-05-17-s7-release-puc-design.md
|
||||||
|
# for the full design rationale.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_OWNER="bryzgalov"
|
||||||
|
REPO_NAME="wp-multi-city"
|
||||||
|
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 wp-multi-city.php and readme.txt"
|
||||||
|
|
||||||
|
sed -i.bak -E \
|
||||||
|
-e "s|^([[:space:]]*\\*[[:space:]]*Version:[[:space:]]*).*$|\\1${VERSION}|" \
|
||||||
|
-e "s|define\\('WPMC_VERSION',[[:space:]]*'[^']+'\\)|define('WPMC_VERSION', '${VERSION}')|" \
|
||||||
|
wp-multi-city.php
|
||||||
|
rm wp-multi-city.php.bak
|
||||||
|
|
||||||
|
grep -q "^ \* Version: ${VERSION}$" wp-multi-city.php \
|
||||||
|
|| fail "bump failed: header Version in wp-multi-city.php"
|
||||||
|
grep -q "define('WPMC_VERSION', '${VERSION}')" wp-multi-city.php \
|
||||||
|
|| fail "bump failed: WPMC_VERSION constant in wp-multi-city.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 wp-multi-city.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="wp-multi-city-${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=wp-multi-city/ \
|
||||||
|
-o "$ZIP_PATH"
|
||||||
|
|
||||||
|
log "Running zip sanity checks"
|
||||||
|
|
||||||
|
unzip -p "$ZIP_PATH" wp-multi-city/wp-multi-city.php \
|
||||||
|
| grep -q "^ \* Version: ${VERSION}$" \
|
||||||
|
|| fail "zip sanity: header Version mismatch inside $ZIP_PATH"
|
||||||
|
|
||||||
|
unzip -p "$ZIP_PATH" wp-multi-city/wp-multi-city.php \
|
||||||
|
| grep -q "WPMC_VERSION', '${VERSION}'" \
|
||||||
|
|| fail "zip sanity: WPMC_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 "wp-multi-city/includes/lib/plugin-update-checker/plugin-update-checker.php" <<< "$ZIP_LISTING" \
|
||||||
|
|| fail "zip sanity: PUC loader missing from $ZIP_PATH"
|
||||||
|
|
||||||
|
grep -q "wp-multi-city/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.dist patchwork.json release.sh CLAUDE.md AGENTS.md .gitattributes .gitignore .gitea/; do
|
||||||
|
if grep -q "wp-multi-city/${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="$(jq -r '.id' <<< "$CREATE_RESPONSE")"
|
||||||
|
[[ -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"
|
||||||
|
|
||||||
|
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" \
|
||||||
|
-o /dev/null
|
||||||
|
|
||||||
|
# Gitea's asset upload response returns an internal /attachments/<uuid> URL,
|
||||||
|
# but the release detail endpoint returns the pretty /releases/download/<tag>/<name>
|
||||||
|
# URL for the same asset. Both resolve to the same bytes; the pretty URL is
|
||||||
|
# preferred for readability and WP-admin UX.
|
||||||
|
RELEASE_DETAIL="$(curl -fsSL \
|
||||||
|
"${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}" \
|
||||||
|
-H "Authorization: token ${TOKEN}")"
|
||||||
|
|
||||||
|
# Use herestring instead of `echo | jq`: `echo` on macOS may interpret
|
||||||
|
# backslash-escapes in a multi-line release body, corrupting the JSON.
|
||||||
|
DOWNLOAD_URL="$(jq -r --arg name "$ZIP_NAME" \
|
||||||
|
'.assets[] | select(.name == $name) | .browser_download_url' \
|
||||||
|
<<< "$RELEASE_DETAIL" | head -n 1)"
|
||||||
|
|
||||||
|
[[ -n "$DOWNLOAD_URL" && "$DOWNLOAD_URL" != "null" ]] \
|
||||||
|
|| fail "release $RELEASE_ID has no asset named $ZIP_NAME"
|
||||||
|
|
||||||
|
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'
|
||||||
Reference in New Issue
Block a user