#!/usr/bin/env bash # # release.sh — Release helper for cf7-spam-to-telegram. # # Usage: ./release.sh # 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 `= =` 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 (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|^(\\s*\\*\\s*Version:\\s*).*$|\\1${VERSION}|" \ -e "s|define\\('CF7STG_VERSION',\\s*'[^']+'\\)|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" unzip -l "$ZIP_PATH" | grep -q "cf7-spam-to-telegram/includes/lib/plugin-update-checker/plugin-update-checker.php" \ || fail "zip sanity: PUC loader missing from $ZIP_PATH" unzip -l "$ZIP_PATH" | grep -q "cf7-spam-to-telegram/wp-plugin-info.json" \ || 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 unzip -l "$ZIP_PATH" | grep -q "cf7-spam-to-telegram/${dev}"; 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) 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 "
  • " escape(pending) "
  • "; pending = "" } if (ul_open) { html = html ""; ul_open = 0 } match($0, /[0-9]+\.[0-9]+\.[0-9]+/) ver = substr($0, RSTART, RLENGTH) html = html "

    " ver "

      " ul_open = 1 next } /^\*[[:space:]]/ { if (pending != "") { html = html "
    • " escape(pending) "
    • " } sub(/^\*[[:space:]]+/, "") pending = $0 next } /^[[:space:]]+[^[:space:]]/ { if (pending != "") { line = $0 sub(/^[[:space:]]+/, "", line) pending = pending " " line } next } /^[[:space:]]*$/ { if (pending != "") { html = html "
    • " escape(pending) "
    • "; pending = "" } next } END { if (pending != "") { html = html "
    • " escape(pending) "
    • " } if (ul_open) { html = html "
    " } 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'