unarr/scripts/ship.sh
Deivid Soto 7a20ddb4ea
Some checks failed
CI / Test (push) Successful in 2m42s
CI / Build (push) Successful in 1m34s
CI / Build-1 (push) Successful in 1m59s
CI / Build-2 (push) Successful in 1m33s
CI / Build-3 (push) Successful in 1m33s
CI / Build-4 (push) Successful in 1m34s
CI / Build-5 (push) Successful in 1m33s
CI / Lint (push) Failing after 2m29s
CI / Coverage (push) Successful in 2m50s
CI / Vet (push) Successful in 2m6s
feat(scripts): prune Forgejo releases >90 days in ship.sh
Adds step 6 to scripts/ship.sh: after smoke checks, list Forgejo
releases and delete any with created_at older than FORGEJO_PRUNE_DAYS
(default 90). Bounded retention prevents the tc-git CPX11 disk from
filling up (each release ≈ 511MB of attachments × 1/week pace).

Skipped silently with a warn if FORGEJO_TOKEN is not exported, so
the step is opt-in via secret presence (no token = no destructive
action). Tunables: FORGEJO_PRUNE_DAYS, FORGEJO_REPO, FORGEJO_BASE,
SKIP_FORGEJO_PRUNE.
2026-05-27 18:19:08 +02:00

222 lines
8.1 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# ship.sh — End-to-end CLI release pipeline.
#
# Standalone backup for when GitHub Actions is unavailable (org shadow-ban,
# CI outage, etc). Mirrors what release.yml + docker job in CI would do.
#
# Pre-requisites:
# - scripts/release.sh already ran → version.go bumped + tag created locally
# - SENTRY_DSN exported (Sentry disabled in build if missing)
# - docker logged in to docker.io as the org user
# - SSH key for Hetzner publishing (see publish-cli-release.sh)
#
# Pipeline:
# 1. Sanity: clean tree, tag at HEAD, version.go matches
# 2. goreleaser build (skip GH publish — produces dist/*)
# 3. Rsync to Hetzner via web/scripts/publish-cli-release.sh
# 4. Multi-arch Docker build + push (amd64 + arm64) to Docker Hub
# 5. Smoke checks (torrentclaw.com/version + docker run image version)
# 6. Prune Forgejo releases older than FORGEJO_PRUNE_DAYS (default 90)
# 7. Optional `git push --follow-tags`
#
# Usage:
# scripts/ship.sh Detect version from internal/cmd/version.go
# scripts/ship.sh 0.9.12 Explicit version
# scripts/ship.sh --dry-run Preview steps, no side effects
# scripts/ship.sh --push 0.9.12 Also git-push tag to GH afterwards
#
# Env knobs:
# SENTRY_DSN telemetry DSN injected at build time
# RELEASE_SIGNING_PUBKEY ed25519 pubkey (base64) for self-update signature check
# DOCKER_IMAGE default torrentclaw/unarr
# PUBLISH_SCRIPT default ../torrentclaw-web/scripts/publish-cli-release.sh
# SKIP_DOCKER=1 skip Docker build/push
# SKIP_HETZNER=1 skip Hetzner publish
# SKIP_SMOKE=1 skip smoke checks
# SKIP_FORGEJO_PRUNE=1 skip Forgejo retention prune
# FORGEJO_TOKEN PAT with write:repository for prune (no token = skip + warn)
# FORGEJO_PRUNE_DAYS retention window, default 90 days
# FORGEJO_REPO default torrentclaw/unarr
#
set -euo pipefail
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_DIR"
DOCKER_IMAGE="${DOCKER_IMAGE:-torrentclaw/unarr}"
PUBLISH_SCRIPT="${PUBLISH_SCRIPT:-$REPO_DIR/../torrentclaw-web/scripts/publish-cli-release.sh}"
SKIP_DOCKER="${SKIP_DOCKER:-0}"
SKIP_HETZNER="${SKIP_HETZNER:-0}"
SKIP_SMOKE="${SKIP_SMOKE:-0}"
SKIP_FORGEJO_PRUNE="${SKIP_FORGEJO_PRUNE:-0}"
FORGEJO_PRUNE_DAYS="${FORGEJO_PRUNE_DAYS:-90}"
FORGEJO_REPO="${FORGEJO_REPO:-torrentclaw/unarr}"
FORGEJO_BASE="${FORGEJO_BASE:-https://git.torrentclaw.com}"
DRY_RUN=false
PUSH_TAG=false
VERSION=""
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
info() { echo -e "${CYAN}${NC} $*"; }
ok() { echo -e "${GREEN}${NC} $*"; }
warn() { echo -e "${YELLOW}${NC} $*"; }
die() { echo -e "${RED}${NC} $*" >&2; exit 1; }
for a in "$@"; do
case "$a" in
--dry-run) DRY_RUN=true ;;
--push) PUSH_TAG=true ;;
-h|--help)
sed -n '2,/^set /p' "$0" | sed 's/^#\s\?//;$d'
exit 0 ;;
[0-9]*) VERSION="$a" ;;
*) die "unknown arg: $a (use --help)" ;;
esac
done
read_version_go() {
grep 'var Version' internal/cmd/version.go | sed 's/.*"\(.*\)".*/\1/'
}
REPO_VERSION="$(read_version_go)"
[ -z "$VERSION" ] && VERSION="$REPO_VERSION"
[ -n "$VERSION" ] || die "cannot detect version (pass explicit X.Y.Z)"
TAG="v$VERSION"
MINOR="${VERSION%.*}"
echo ""
echo -e " ${BOLD}Ship Plan${NC}"
echo -e " ─────────────────────────────"
echo -e " Version: ${GREEN}$TAG${NC}"
echo -e " Docker image: $DOCKER_IMAGE:{$VERSION,$MINOR,latest}"
echo -e " Skip Hetzner: $SKIP_HETZNER"
echo -e " Skip Docker: $SKIP_DOCKER"
echo -e " Push to GH: $PUSH_TAG"
echo -e " Dry run: $DRY_RUN"
echo ""
# Sanity
[ "$REPO_VERSION" = "$VERSION" ] || die "version.go=$REPO_VERSION ≠ requested $VERSION (bump with make release-* first)"
if [ "$DRY_RUN" = false ]; then
[ -z "$(git status --porcelain)" ] || die "working tree dirty"
git rev-parse "$TAG" >/dev/null 2>&1 || die "tag $TAG missing — run scripts/release.sh first"
HEAD_SHA="$(git rev-parse HEAD)"
TAG_SHA="$(git rev-parse "$TAG^{commit}")"
[ "$HEAD_SHA" = "$TAG_SHA" ] || die "HEAD ($HEAD_SHA) ≠ tag commit ($TAG_SHA) — checkout $TAG first"
command -v goreleaser >/dev/null || die "goreleaser not installed"
[ "$SKIP_DOCKER" = "1" ] || command -v docker >/dev/null || die "docker not installed"
[ "$SKIP_HETZNER" = "1" ] || [ -x "$PUBLISH_SCRIPT" ] || die "publish script missing or not executable: $PUBLISH_SCRIPT"
if [ -z "${SENTRY_DSN:-}" ]; then
warn "SENTRY_DSN unset — built binaries will have Sentry disabled"
fi
fi
if [ "$DRY_RUN" = true ]; then
ok "Dry run complete — no changes made"
exit 0
fi
# 1. Build
info "goreleaser build ($TAG)"
SENTRY_DSN="${SENTRY_DSN:-}" RELEASE_SIGNING_PUBKEY="${RELEASE_SIGNING_PUBKEY:-}" \
goreleaser release --clean --skip=publish
ok "dist/ ready"
# 2. Hetzner
if [ "$SKIP_HETZNER" != "1" ]; then
info "publishing to Hetzner releases volume"
"$PUBLISH_SCRIPT" "$VERSION"
ok "Hetzner version.txt flipped to $VERSION"
fi
# 3. Docker
if [ "$SKIP_DOCKER" != "1" ]; then
info "docker buildx multi-arch push ($DOCKER_IMAGE:$VERSION, :$MINOR, :latest)"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg VERSION="$TAG" \
-t "$DOCKER_IMAGE:$VERSION" \
-t "$DOCKER_IMAGE:$MINOR" \
-t "$DOCKER_IMAGE:latest" \
--push .
ok "Docker Hub: $DOCKER_IMAGE:{$VERSION,$MINOR,latest}"
fi
# 4. Smoke
if [ "$SKIP_SMOKE" != "1" ]; then
info "smoke checks"
if [ "$SKIP_HETZNER" != "1" ]; then
LIVE_VERSION="$(curl -fsSL https://torrentclaw.com/version 2>/dev/null | tr -d '[:space:]' || echo '')"
if [ "$LIVE_VERSION" = "$VERSION" ]; then
ok "torrentclaw.com/version = $LIVE_VERSION"
else
warn "torrentclaw.com/version = '$LIVE_VERSION' (expected $VERSION)"
fi
fi
if [ "$SKIP_DOCKER" != "1" ]; then
DOCKER_VERSION="$(docker run --rm "$DOCKER_IMAGE:$VERSION" version 2>/dev/null | grep -oE 'v[0-9.]+' | head -1)"
if [ "$DOCKER_VERSION" = "$TAG" ]; then
ok "docker image $DOCKER_IMAGE:$VERSION reports $DOCKER_VERSION"
else
warn "docker image reports '$DOCKER_VERSION' (expected $TAG)"
fi
fi
fi
# 6. Forgejo retention prune
if [ "$SKIP_FORGEJO_PRUNE" != "1" ]; then
if [ -z "${FORGEJO_TOKEN:-}" ]; then
warn "FORGEJO_TOKEN not set — skipping Forgejo prune (set it to enable >${FORGEJO_PRUNE_DAYS}-day cleanup)"
else
info "pruning Forgejo releases older than $FORGEJO_PRUNE_DAYS days"
FORGEJO_API="$FORGEJO_BASE/api/v1/repos/$FORGEJO_REPO/releases"
RELEASES_JSON="$(curl -fsSL -H "Authorization: token $FORGEJO_TOKEN" "$FORGEJO_API?limit=50" || echo '[]')"
PRUNE_IDS="$(echo "$RELEASES_JSON" | python3 -c "
import json, sys
from datetime import datetime, timedelta, timezone
days = int('${FORGEJO_PRUNE_DAYS}')
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
for r in json.load(sys.stdin):
created = datetime.fromisoformat(r['created_at'].replace('Z', '+00:00'))
if created < cutoff:
print(f\"{r['id']}\t{r['tag_name']}\t{r['created_at']}\")
" 2>/dev/null || true)"
DELETED=0
FAILED=0
if [ -n "$PRUNE_IDS" ]; then
while IFS=$'\t' read -r REL_ID REL_TAG REL_CREATED; do
[ -z "$REL_ID" ] && continue
CODE="$(curl -s -o /dev/null -w '%{http_code}' -X DELETE -H "Authorization: token $FORGEJO_TOKEN" "$FORGEJO_API/$REL_ID")"
if [ "$CODE" = "204" ]; then
echo " deleted $REL_TAG (created $REL_CREATED)"
DELETED=$((DELETED + 1))
else
warn " failed to delete $REL_TAG (id=$REL_ID, http=$CODE)"
FAILED=$((FAILED + 1))
fi
done <<< "$PRUNE_IDS"
fi
if [ "$FAILED" -gt 0 ]; then
warn "Forgejo prune: $DELETED removed, $FAILED failed"
else
ok "Forgejo prune: $DELETED release(s) removed (>${FORGEJO_PRUNE_DAYS} days old)"
fi
fi
fi
# 7. Optional push
if [ "$PUSH_TAG" = true ]; then
info "git push origin main --follow-tags"
git push origin main --follow-tags
ok "tag $TAG pushed to GitHub"
fi
echo ""
ok "${BOLD}$TAG shipped${NC}"