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.
172 lines
5.9 KiB
Bash
Executable file
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}"
|