mateclaw/mateclaw-desktop/scripts/publish-github.sh
2026-07-12 17:30:23 +08:00

325 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Publish MateClaw Desktop release artifacts to GitHub Releases
#
# Prerequisites:
# - gh CLI installed and authenticated (gh auth login)
# - Build artifacts exist in release/ directory (run build-all-platforms.sh first)
#
# Usage:
# bash scripts/publish-github.sh # Create release (direct)
# bash scripts/publish-github.sh --proxy # Create release via proxy (127.0.0.1:7890)
# bash scripts/publish-github.sh --proxy=host:port # Create release via custom proxy
# bash scripts/publish-github.sh --draft # Create draft release
# bash scripts/publish-github.sh --tag=v1.1.0 # Custom tag (default: v{version} from package.json)
# bash scripts/publish-github.sh --retry # Delete existing release and re-upload
# bash scripts/publish-github.sh --proxy --retry --draft # Combine options
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DESKTOP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
RELEASE_DIR="$DESKTOP_DIR/release"
# ─── Parse arguments ───────────────────────────────────────────────────────
DRAFT=""
CUSTOM_TAG=""
USE_PROXY=""
PROXY_ADDR="127.0.0.1:7890"
RETRY=false
NOTES_FILE_OVERRIDE=""
NOTES_LANG="en"
for arg in "$@"; do
case "$arg" in
--draft) DRAFT="--draft" ;;
--tag=*) CUSTOM_TAG="${arg#--tag=}" ;;
--proxy) USE_PROXY=true ;;
--proxy=*) USE_PROXY=true; PROXY_ADDR="${arg#--proxy=}" ;;
--retry) RETRY=true ;;
--notes-file=*) NOTES_FILE_OVERRIDE="${arg#--notes-file=}" ;;
--notes-lang=*) NOTES_LANG="${arg#--notes-lang=}" ;;
-h|--help)
echo "Usage: $0 [--draft] [--tag=vX.Y.Z] [--proxy[=host:port]] [--retry] [--notes-file=path] [--notes-lang=en|zh]"
echo ""
echo "Options:"
echo " --draft Create as draft release"
echo " --tag=vX.Y.Z Custom tag (default: v{version} from package.json)"
echo " --proxy Use proxy 127.0.0.1:7890 for uploading to GitHub"
echo " --proxy=host:port Use custom proxy address"
echo " --retry Delete existing release for this tag before uploading"
echo " --notes-file=path Use this markdown file as release notes (highest priority)"
echo " --notes-lang=en|zh Preferred language for auto-detected notes (default: en)"
echo ""
echo "Release notes resolution order:"
echo " 1. --notes-file=path (explicit override)"
echo " 2. ../docs/{notes-lang}/releases/X.Y.Z.md (curated, preferred lang)"
echo " 3. ../docs/{other-lang}/releases/X.Y.Z.md (curated, fallback lang)"
echo " 4. Tag annotation (if multi-line) (legacy)"
echo " 5. Tagged commit message body (legacy)"
echo " 6. --generate-notes (GitHub auto)"
echo ""
echo "Examples:"
echo " # Step 1: Build locally (no publish)"
echo " bash scripts/build-all-platforms.sh --all"
echo ""
echo " # Step 2: Publish to GitHub"
echo " bash scripts/publish-github.sh --proxy # via proxy, en notes"
echo " bash scripts/publish-github.sh --proxy --notes-lang=zh # zh notes"
echo " bash scripts/publish-github.sh --proxy --draft # draft (preview)"
echo " bash scripts/publish-github.sh --proxy --retry # delete + re-upload"
echo " bash scripts/publish-github.sh --notes-file=NOTES.md # custom notes file"
echo ""
echo "Run build-all-platforms.sh first to generate artifacts."
exit 0
;;
*)
echo "Unknown option: $arg"
echo "Usage: $0 [--draft] [--tag=vX.Y.Z] [--proxy[=host:port]] [--retry] [--notes-file=path] [--notes-lang=en|zh]"
exit 1
;;
esac
done
if [ "$NOTES_LANG" != "en" ] && [ "$NOTES_LANG" != "zh" ]; then
echo "❌ --notes-lang must be 'en' or 'zh' (got: $NOTES_LANG)"
exit 1
fi
# ─── Setup proxy ───────────────────────────────────────────────────────────
if [ "$USE_PROXY" = true ]; then
export https_proxy="http://${PROXY_ADDR}"
export http_proxy="http://${PROXY_ADDR}"
export HTTPS_PROXY="http://${PROXY_ADDR}"
export HTTP_PROXY="http://${PROXY_ADDR}"
echo "🌐 Proxy enabled: ${PROXY_ADDR}"
echo ""
fi
# ─── Check prerequisites ──────────────────────────────────────────────────
if ! command -v gh &>/dev/null; then
echo "❌ gh CLI not found. Install: https://cli.github.com/"
exit 1
fi
if ! gh auth status &>/dev/null; then
echo "❌ gh not authenticated. Run: gh auth login"
exit 1
fi
# ─── Read version from package.json ───────────────────────────────────────
VERSION=$(node -p "require('$DESKTOP_DIR/package.json').version")
TAG="${CUSTOM_TAG:-v${VERSION}}"
echo "╔════════════════════════════════════════════════════════╗"
echo "║ MateClaw Desktop - Publish to GitHub ║"
echo "╚════════════════════════════════════════════════════════╝"
echo ""
echo " Version: $VERSION"
echo " Tag: $TAG"
echo " Mode: ${DRAFT:-release}"
echo " Proxy: ${USE_PROXY:+${PROXY_ADDR}}${USE_PROXY:-off}"
echo " Retry: $RETRY"
echo ""
# ─── Retry: delete existing release ───────────────────────────────────────
if [ "$RETRY" = true ]; then
echo "🔄 Checking for existing release $TAG ..."
if gh release view "$TAG" &>/dev/null; then
echo " Found existing release, deleting..."
gh release delete "$TAG" --yes --cleanup-tag 2>/dev/null || gh release delete "$TAG" --yes 2>/dev/null
echo " ✅ Old release deleted."
else
echo " No existing release found, proceeding."
fi
echo ""
fi
# ─── Collect release artifacts ─────────────────────────────────────────────
if [ ! -d "$RELEASE_DIR" ]; then
echo "❌ Release directory not found: $RELEASE_DIR"
echo " Run build-all-platforms.sh first."
exit 1
fi
ARTIFACTS=()
# macOS artifacts
for pattern in "MateClaw*.dmg" "MateClaw*.zip"; do
while IFS= read -r -d '' f; do
ARTIFACTS+=("$f")
done < <(find "$RELEASE_DIR" -maxdepth 1 -name "$pattern" -print0 2>/dev/null)
done
# Windows artifacts (skip the architecture-merged installer — only ship per-arch builds)
while IFS= read -r -d '' f; do
ARTIFACTS+=("$f")
done < <(find "$RELEASE_DIR" -maxdepth 1 -name "MateClaw*Setup*.exe" \
! -name "MateClaw_${VERSION}_Setup.exe" -print0 2>/dev/null)
# Linux artifacts
while IFS= read -r -d '' f; do
ARTIFACTS+=("$f")
done < <(find "$RELEASE_DIR" -maxdepth 1 -name "MateClaw*.AppImage" -print0 2>/dev/null)
# Auto-update metadata files
for pattern in "latest-mac.yml" "latest.yml" "latest-linux.yml"; do
if [ -f "$RELEASE_DIR/$pattern" ]; then
ARTIFACTS+=("$RELEASE_DIR/$pattern")
fi
done
# blockmap files (for differential updates) — also skip merged installer's blockmap
while IFS= read -r -d '' f; do
ARTIFACTS+=("$f")
done < <(find "$RELEASE_DIR" -maxdepth 1 -name "*.blockmap" \
! -name "MateClaw_${VERSION}_Setup.exe.blockmap" -print0 2>/dev/null)
if [ ${#ARTIFACTS[@]} -eq 0 ]; then
echo "❌ No release artifacts found in $RELEASE_DIR"
echo " Run build-all-platforms.sh first."
exit 1
fi
echo " Artifacts to upload:"
TOTAL_SIZE=0
for f in "${ARTIFACTS[@]}"; do
SIZE_BYTES=$(stat -f%z "$f" 2>/dev/null || stat -c%s "$f" 2>/dev/null || echo 0)
SIZE_HUMAN=$(du -h "$f" | cut -f1)
TOTAL_SIZE=$((TOTAL_SIZE + SIZE_BYTES))
echo "$(basename "$f") ($SIZE_HUMAN)"
done
TOTAL_HUMAN=$(echo "$TOTAL_SIZE" | awk '{
if ($1 >= 1073741824) printf "%.1f GB", $1/1073741824;
else if ($1 >= 1048576) printf "%.0f MB", $1/1048576;
else printf "%.0f KB", $1/1024;
}')
echo ""
echo " Total: $TOTAL_HUMAN"
echo ""
# ─── Resolve release notes (priority chain) ───────────────────────────────
# 1. --notes-file=path (explicit override)
# 2. ../docs/{notes-lang}/releases/X.Y.Z.md (curated, preferred lang)
# 3. ../docs/{other-lang}/releases/X.Y.Z.md (curated, fallback lang)
# 4. Tag annotation (if multi-line) (legacy fallback)
# 5. Tagged commit message body (legacy fallback)
# 6. --generate-notes (GitHub auto)
NOTES_FILE=$(mktemp)
trap "rm -f '$NOTES_FILE'" EXIT
REPO_ROOT="$(cd "$DESKTOP_DIR/.." && pwd)"
NOTES_LANG_OTHER=$([ "$NOTES_LANG" = "en" ] && echo "zh" || echo "en")
NOTES_PRIMARY="$REPO_ROOT/docs/${NOTES_LANG}/releases/${VERSION}.md"
NOTES_FALLBACK="$REPO_ROOT/docs/${NOTES_LANG_OTHER}/releases/${VERSION}.md"
if [ -n "$NOTES_FILE_OVERRIDE" ]; then
if [ ! -f "$NOTES_FILE_OVERRIDE" ]; then
echo "❌ --notes-file path not found: $NOTES_FILE_OVERRIDE"
exit 1
fi
cat "$NOTES_FILE_OVERRIDE" > "$NOTES_FILE"
echo "📝 Using --notes-file: $NOTES_FILE_OVERRIDE"
elif [ -f "$NOTES_PRIMARY" ]; then
cat "$NOTES_PRIMARY" > "$NOTES_FILE"
echo "📝 Using docs/${NOTES_LANG}/releases/${VERSION}.md as release notes."
elif [ -f "$NOTES_FALLBACK" ]; then
cat "$NOTES_FALLBACK" > "$NOTES_FILE"
echo "📝 Using docs/${NOTES_LANG_OTHER}/releases/${VERSION}.md as release notes (preferred '$NOTES_LANG' not found)."
else
# Legacy fallback: tag annotation, then commit body
TAG_MSG=$(git tag -l --format='%(contents)' "$TAG" 2>/dev/null | sed '/^$/d')
if [ -n "$TAG_MSG" ] && [ "$(echo "$TAG_MSG" | wc -l)" -gt 2 ]; then
echo "$TAG_MSG" > "$NOTES_FILE"
echo "📝 Using tag annotation as release notes."
else
COMMIT_MSG=$(git log -1 --format='%B' "$TAG" 2>/dev/null | tail -n +2 | sed '/^$/d')
if [ -n "$COMMIT_MSG" ]; then
echo "$COMMIT_MSG" > "$NOTES_FILE"
echo "📝 Using commit message as release notes."
else
echo "⚠️ No release notes found (no docs/X.Y.Z.md, tag annotation, or commit body), using --generate-notes."
fi
fi
fi
echo ""
# ─── Rewrite latest.yml so it does not reference the merged installer ─────
#
# electron-builder generates a multi-arch merged Windows installer
# (MateClaw_X.Y.Z_Setup.exe, ~600 MB) alongside the per-arch installers
# (MateClaw_X.Y.Z_x64_Setup.exe, MateClaw_X.Y.Z_arm64_Setup.exe). The
# merged build is too large to be worth uploading, but the default
# latest.yml top-level `path:` and `files[0]` both point at it. If we
# upload latest.yml as-is while skipping the merged exe, electron-updater
# on Windows hits 404 on every check and the whole update path breaks.
#
# Fix: rewrite latest.yml in place so `path` / `sha512` / `files[]` only
# reference assets that are actually being uploaded. Use x64 as the
# default top-level target (arm64 clients still match via files[]).
LATEST_YML="$RELEASE_DIR/latest.yml"
MERGED_EXE_NAME="MateClaw_${VERSION}_Setup.exe"
X64_EXE_NAME="MateClaw_${VERSION}_x64_Setup.exe"
ARM64_EXE_NAME="MateClaw_${VERSION}_arm64_Setup.exe"
if [ -f "$LATEST_YML" ]; then
echo "🧹 Rewriting latest.yml to drop the merged installer ..."
(cd "$DESKTOP_DIR" && node -e "
const fs = require('fs');
const yaml = require('js-yaml');
const file = '$LATEST_YML';
const merged = '$MERGED_EXE_NAME';
const x64 = '$X64_EXE_NAME';
const arm64 = '$ARM64_EXE_NAME';
const doc = yaml.load(fs.readFileSync(file, 'utf8'));
const before = (doc.files || []).length;
doc.files = (doc.files || []).filter(f => f.url !== merged);
const x64Entry = doc.files.find(f => f.url === x64);
const arm64Entry = doc.files.find(f => f.url === arm64);
if (!x64Entry || !arm64Entry) {
console.error('FATAL: per-arch installer entries missing from latest.yml');
console.error(' files:', doc.files.map(f => f.url));
process.exit(1);
}
doc.path = x64Entry.url;
doc.sha512 = x64Entry.sha512;
fs.writeFileSync(file, yaml.dump(doc, { lineWidth: -1 }));
console.log(' files[] entries: ' + before + ' → ' + doc.files.length);
console.log(' top-level path: ' + doc.path);
")
echo ""
fi
# ─── Create GitHub Release ─────────────────────────────────────────────────
echo "🚀 Creating GitHub Release $TAG ..."
echo ""
if [ -s "$NOTES_FILE" ]; then
gh release create "$TAG" \
--title "MateClaw $VERSION" \
--notes-file "$NOTES_FILE" \
$DRAFT \
"${ARTIFACTS[@]}"
else
gh release create "$TAG" \
--title "MateClaw $VERSION" \
--generate-notes \
$DRAFT \
"${ARTIFACTS[@]}"
fi
echo ""
echo "✅ Release published successfully!"
echo ""
gh release view "$TAG" --json url -q '.url'