unarr/scripts/ship.sh
Deivid Soto 23b79f6411 chore(release): add ship.sh end-to-end pipeline as GH Actions backup
GitHub Actions release.yml + docker job currently doesn't fire (org
shadow-ban). ship.sh replicates the CI pipeline locally so releases
keep landing on Hetzner + Docker Hub without depending on CI:

  1. Sanity checks: clean tree, tag at HEAD, version.go match
  2. goreleaser release --skip=publish  (build dist/*)
  3. publish-cli-release.sh  (rsync to Hetzner + flip version.txt)
  4. docker buildx --push multi-arch (amd64 + arm64)
  5. Smoke: torrentclaw.com/version + docker run image version
  6. Optional --push to git-push tag to GH

Exposed via make targets: ship, ship-dry, ship-push.
2026-05-27 12:35:01 +02:00

172 lines
5.9 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. 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
#
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}"
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
# 5. 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}"