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.
This commit is contained in:
parent
80461ea7fe
commit
23b79f6411
2 changed files with 186 additions and 1 deletions
172
scripts/ship.sh
Executable file
172
scripts/ship.sh
Executable file
|
|
@ -0,0 +1,172 @@
|
|||
#!/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}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue