Files
goclaw/scripts/goclaw-upgrade-release.sh
T
Duy /zuey/ faa195156e feat(ci): deploy zuey beta releases (#40)
* feat(ci): deploy zuey beta releases

Add automatic zuey VPS deployment to the dev beta release workflow after prerelease assets are published. Publish beta checksums and make the host upgrade script tolerate beta asset naming and checksum fallback.

* fix(deploy): allow active beta release reruns

Treat an existing target release as success only when it is already the active release and still contains the expected binary and migrations. This keeps automated beta deploy reruns from failing after a prior successful deploy while preserving fail-closed behavior for stale or partial release directories.
2026-05-22 19:49:43 +07:00

229 lines
6.4 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
REPO="digitopvn/goclaw"
BASE_DIR="/opt/goclaw"
RELEASES_DIR="${BASE_DIR}/releases"
DEPLOY_BIN="/usr/local/bin/goclaw-deploy"
STATUS_DIR="/var/lib/goclaw/update-jobs"
STATUS_FILE="${STATUS_DIR}/current.json"
STATUS_OWNER="${GOCLAW_STATUS_OWNER:-goclaw:goclaw}"
DRY_RUN=0
log() { printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*"; }
json_escape() { python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"; }
write_status() {
local state="$1" requested="$2" resolved="$3" error_msg="${4:-}"
mkdir -p "$STATUS_DIR"
local now job target before tmp
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
job="$(date -u +%Y%m%dT%H%M%SZ)-${resolved:-$requested}"
target="${RELEASES_DIR}/${resolved:-$requested}"
before=""
if [ -L "${BASE_DIR}/current" ]; then
before="$(readlink -f "${BASE_DIR}/current" || true)"
fi
local finished_json
if [ "$state" = "running" ]; then
finished_json="null"
else
finished_json="$(json_escape "$now")"
fi
tmp="$(mktemp "${STATUS_DIR}/current.XXXXXX")"
cat > "$tmp" <<JSON
{
"jobId": $(json_escape "$job"),
"state": $(json_escape "$state"),
"requestedTag": $(json_escape "$requested"),
"resolvedTag": $(json_escape "$resolved"),
"startedAt": $(json_escape "$now"),
"finishedAt": ${finished_json},
"currentReleaseBefore": $(json_escape "$before"),
"targetRelease": $(json_escape "$target"),
"error": $(json_escape "$error_msg")
}
JSON
chown "$STATUS_OWNER" "$tmp" 2>/dev/null || true
chmod 0640 "$tmp"
mv "$tmp" "$STATUS_FILE"
}
fail() {
local msg="$1"
log "ERROR: $msg"
if [ "$DRY_RUN" != "1" ]; then
write_status "failed" "${REQUESTED_TAG:-}" "${RESOLVED_TAG:-}" "$msg" || true
fi
exit 1
}
usage() {
cat <<'EOF'
usage: goclaw-upgrade-release [--dry-run] <latest|vMAJOR.MINOR.PATCH[-beta.N|-rc.N]>
EOF
}
if [ "${1:-}" = "--dry-run" ]; then
DRY_RUN=1
shift
fi
REQUESTED_TAG="${1:-}"
if [ -z "$REQUESTED_TAG" ] || [ "${2:-}" != "" ]; then
usage >&2
exit 2
fi
if [ "$REQUESTED_TAG" != "latest" ] && ! [[ "$REQUESTED_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(beta|rc)\.[0-9]+)?$ ]]; then
fail "invalid tag"
fi
require_bin() { command -v "$1" >/dev/null 2>&1 || fail "missing dependency: $1"; }
require_bin curl
require_bin flock
require_bin tar
require_bin sha256sum
require_bin python3
if [ "$DRY_RUN" != "1" ]; then
mkdir -p "$STATUS_DIR"
exec 9>"${STATUS_DIR}/upgrade.lock"
flock -n 9 || fail "gateway upgrade already running"
fi
RESOLVED_TAG="$REQUESTED_TAG"
if [ "$REQUESTED_TAG" = "latest" ]; then
log "resolving latest stable server release"
RESOLVED_TAG="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases?per_page=50" | python3 -c '
import json, re, sys
for rel in json.load(sys.stdin):
tag = rel.get("tag_name", "")
if rel.get("draft") or rel.get("prerelease"):
continue
if re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+", tag):
print(tag)
raise SystemExit(0)
raise SystemExit("no stable server release found")
')"
fi
if ! [[ "$RESOLVED_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(beta|rc)\.[0-9]+)?$ ]]; then
fail "resolved invalid tag: $RESOLVED_TAG"
fi
VERSION="${RESOLVED_TAG#v}"
ASSET=""
ASSET_URL=""
CHECKSUM_URL="https://github.com/${REPO}/releases/download/${RESOLVED_TAG}/CHECKSUMS.sha256"
TARGET_DIR="${RELEASES_DIR}/${RESOLVED_TAG}"
download_release_asset() {
local candidate url
for candidate in "goclaw-${VERSION}-linux-amd64.tar.gz" "goclaw-${RESOLVED_TAG}-linux-amd64.tar.gz"; do
url="https://github.com/${REPO}/releases/download/${RESOLVED_TAG}/${candidate}"
log "downloading release asset candidate=${candidate}"
if curl -fsSL -o "$candidate" "$url"; then
ASSET="$candidate"
ASSET_URL="$url"
return 0
fi
rm -f "$candidate"
done
fail "linux amd64 release asset not found for ${RESOLVED_TAG}"
}
github_release_asset_digest() {
local asset_name="$1"
curl -fsSL "https://api.github.com/repos/${REPO}/releases/tags/${RESOLVED_TAG}" | python3 -c '
import json
import sys
name = sys.argv[1]
release = json.load(sys.stdin)
for asset in release.get("assets", []):
if asset.get("name") == name:
digest = asset.get("digest", "")
if digest.startswith("sha256:"):
print(digest.split(":", 1)[1])
raise SystemExit(0)
raise SystemExit(1)
' "$asset_name"
}
verify_release_asset() {
if curl -fsSLO "$CHECKSUM_URL"; then
if grep " ${ASSET}$\|${ASSET}$" CHECKSUMS.sha256 | sha256sum -c -; then
log "checksum verified via CHECKSUMS.sha256"
return 0
fi
log "checksum file did not verify ${ASSET}; falling back to release asset digest"
else
log "CHECKSUMS.sha256 unavailable; falling back to release asset digest"
fi
local expected actual
if ! expected="$(github_release_asset_digest "$ASSET")"; then
fail "missing sha256 digest for ${ASSET}"
fi
read -r actual _ < <(sha256sum "$ASSET")
if [ "$actual" != "$expected" ]; then
fail "release asset digest verification failed"
fi
log "checksum verified via GitHub release asset digest"
}
log "requested=${REQUESTED_TAG} resolved=${RESOLVED_TAG}"
target_release_is_active() {
[ -d "$TARGET_DIR" ] || return 1
[ -L "${BASE_DIR}/current" ] || return 1
[ "$(readlink -f "${BASE_DIR}/current")" = "$(readlink -f "$TARGET_DIR")" ]
}
if [ "$DRY_RUN" = "1" ]; then
TMP_DIR="$(mktemp -d)"
cleanup() { rm -rf "$TMP_DIR"; }
trap cleanup EXIT
cd "$TMP_DIR"
download_release_asset
verify_release_asset
log "dry-run ok"
exit 0
fi
if target_release_is_active; then
if [ ! -x "$TARGET_DIR/goclaw" ] || [ ! -d "$TARGET_DIR/migrations" ]; then
fail "active release is missing goclaw binary or migrations directory"
fi
log "target release already active: ${RESOLVED_TAG}"
write_status "succeeded" "$REQUESTED_TAG" "$RESOLVED_TAG" ""
exit 0
fi
write_status "running" "$REQUESTED_TAG" "$RESOLVED_TAG" ""
TMP_DIR="$(mktemp -d)"
cleanup() { rm -rf "$TMP_DIR"; }
trap cleanup EXIT
cd "$TMP_DIR"
download_release_asset
verify_release_asset
if [ -e "$TARGET_DIR" ]; then
fail "target release already exists: $TARGET_DIR"
fi
mkdir -p "$TARGET_DIR"
tar -xzf "$ASSET" -C "$TARGET_DIR"
chmod +x "$TARGET_DIR/goclaw"
if [ ! -x "$TARGET_DIR/goclaw" ] || [ ! -d "$TARGET_DIR/migrations" ]; then
fail "release archive missing goclaw binary or migrations directory"
fi
log "deploying ${RESOLVED_TAG}"
"$DEPLOY_BIN" "$TARGET_DIR" || fail "deploy failed"
write_status "succeeded" "$REQUESTED_TAG" "$RESOLVED_TAG" ""
log "upgrade complete: ${RESOLVED_TAG}"