unarr/.claude/commands/publish.md
Deivid Soto 287685427a chore(skills): add /publish slash command + allow .claude/ in git
Mirrors the slash command added in torrentclaw-web/.claude/commands.
With the global ~/.gitignore excluding .claude/ by default, the
gitignore override is required for project-shared commands/agents/hooks
to be checked in (settings.local.json and projects/ stay local).

/publish documents the full unarr release flow (bump + tag + binaries +
Hetzner + Docker Hub + smoke) as a single command, while GitHub Actions
remains unavailable for the torrentclaw org.
2026-05-27 12:46:24 +02:00

7.5 KiB
Raw Blame History

description argument-hint
Release unarr CLI end-to-end (bump + tag + binaries + Hetzner + Docker Hub + smoke). Standalone, does not depend on GitHub Actions. [patch|minor|major|X.Y.Z] [--push] [--dry-run] [--skip-tests]

Publish — unarr CLI end-to-end release

Ships a new unarr CLI release across every distribution channel TorrentClaw operates: the self-hosted Hetzner releases volume (/opt/torrentclaw/releases), Docker Hub (torrentclaw/unarr multi-arch), and optionally a GitHub tag push. The pipeline is implemented in torrentclaw-cli/scripts/ship.sh and orchestrated here.

Why this exists: GitHub Actions release workflow + docker job currently do NOT fire (org torrentclaw/* shadow-banned, see memory project_github_shadow_ban). Until support resolves it, this command is the canonical release path.

Repo layout

This command spans two repos:

Repo Path Role
torrentclaw-cli /home/buryni/Proyectos/torrentclaw/torrentclaw-cli Source, Makefile (release.sh, ship.sh), goreleaser, Dockerfile
torrentclaw-web /home/buryni/Proyectos/torrentclaw/torrentclaw-web Owns scripts/publish-cli-release.sh (Hetzner rsync) — invoked by ship.sh

All commands below run from the CLI repo root unless noted.

Inputs (from $ARGUMENTS)

  • Positional bump: patch (default), minor, major, or explicit X.Y.Z
  • --push — also git push origin main --follow-tags after publishing (creates GH tag for the day shadow-ban lifts; harmless if Actions stays silent)
  • --dry-run — preview every step, mutate nothing
  • --skip-tests — skip go test step (use ONLY for emergency reships of an already-validated tree)

Pre-flight (always run, even on --dry-run)

  1. Identify branch + tree:

    cd /home/buryni/Proyectos/torrentclaw/torrentclaw-cli
    git rev-parse --abbrev-ref HEAD
    git status --short
    

    Must be on main with a clean tree. If dirty, stop and surface what's uncommitted — do not auto-stash.

  2. Toolchain check:

    command -v goreleaser go docker git git-cliff
    docker buildx ls | head -3
    docker login --get-login 2>/dev/null || head -c 200 ~/.docker/config.json
    

    Need torrentclaw logged in to index.docker.io. If missing, stop and ask.

  3. Secrets present:

    [ -n "$SENTRY_DSN" ] && echo "SENTRY_DSN: set" || echo "SENTRY_DSN: MISSING"
    

    The Sentry DSN lives in memory reference_cli_release.md. If unset, export it before invoking ship.sh:

    export SENTRY_DSN="https://a190108e4b5dbab517f689885179fbd7@o4511124663894016.ingest.de.sentry.io/4511124676477008"
    

    Missing DSN = built binaries silently disable Sentry. Acceptable but warn.

Validate (unless --skip-tests)

go vet ./...
go test ./...

Stop on any failure. Don't release a broken tree.

Step 1 — Bump + tag (creates a chore(release): X.Y.Z commit and vX.Y.Z annotated tag)

Pick the bump from $ARGUMENTS. Default is patch.

make release-patch              # auto from latest tag
# OR
make release V=0.9.12           # explicit

scripts/release.sh is interactive — it shows the changelog preview and asks y/N. Pipe y:

echo y | make release-patch

After this step:

  • internal/cmd/version.go shows new version
  • CHANGELOG.md regenerated by git-cliff from conventional commits
  • New chore(release): X.Y.Z commit on main
  • New annotated tag vX.Y.Z at HEAD

If --dry-run: run make release-dry V=… instead and stop after this step.

Step 2 — Ship (binaries + Hetzner + Docker Hub + smoke)

SENTRY_DSN="…" make ship           # without --push
SENTRY_DSN="…" make ship-push      # adds git push at the end

scripts/ship.sh does, in order:

  1. Re-checks tree clean, tag exists at HEAD, version.go matches
  2. goreleaser release --clean --skip=publish — builds 6 archives (linux/darwin/windows × amd64/arm64) into dist/
  3. ../torrentclaw-web/scripts/publish-cli-release.sh $V — rsync archives to root@100.117.187.33:/opt/torrentclaw/releases/v$V/ over Tailscale, then flips version.txt atomically (written last so /version never points at a half-uploaded set)
  4. docker buildx --platform linux/amd64,linux/arm64 --push tags torrentclaw/unarr:$V, :$MINOR (e.g. 0.9), :latest
  5. Smoke probes:
    • curl torrentclaw.com/version must equal $VERSION
    • docker run --rm torrentclaw/unarr:$V version must equal v$VERSION

Escape hatches if a step needs skipping (debugging, partial reship):

  • SKIP_HETZNER=1 — skip Hetzner rsync
  • SKIP_DOCKER=1 — skip Docker build/push
  • SKIP_SMOKE=1 — skip the curl + docker run probes

Step 3 — Post-publish verification (independent of ship.sh smoke)

After make ship exits clean, confirm externally:

# Canonical version endpoint (no CF cache — cf-cache-status: DYNAMIC)
curl -fsSL https://torrentclaw.com/version

# get. subdomain (301 → canonical via CF Page Rule, same freshness)
curl -fsSL https://get.torrentclaw.com/version

# Install script is reachable (cache-control: no-store)
curl -fsSL https://torrentclaw.com/install.sh | head -3

# Docker Hub manifest (multi-arch)
docker buildx imagetools inspect torrentclaw/unarr:$V | head -20

# A real install path: download + extract one archive to /tmp + run
tmpdir=$(mktemp -d) && curl -fsSL https://torrentclaw.com/releases/download/v$V/unarr_${V}_linux_amd64.tar.gz | tar -xz -C $tmpdir && $tmpdir/unarr version

All four must agree on $V. If torrentclaw.com/version reports the old version, publish-cli-release.sh likely failed mid-flight — re-run make ship. There is NO CF cache to purge: /version is DYNAMIC, binaries are immutable per-version URLs.

Step 4 — Optional GH push (if --push was passed and not done by ship-push)

git push origin main --follow-tags

This pushes the chore(release) commit + the vX.Y.Z tag. CI workflows (release.yml + docker) would normally fire here. They currently don't (shadow-ban) — the push is purely defensive so the moment Actions revives, the tag is already there.

Output to user

After the run, surface:

  • Version shipped (vX.Y.Z)
  • Live version on torrentclaw.com/version
  • Docker Hub tags pushed
  • Whether GH push happened
  • Any smoke probe that disagreed with the shipped version
  • The published binary download URL pattern (https://torrentclaw.com/releases/download/v$V/unarr_${V}_<os>_<arch>.{tar.gz,zip})

If anything failed mid-pipeline, explain WHERE in the 5 ship.sh steps the failure happened and the exact command to resume from (e.g. SKIP_GORELEASER is not a thing — re-run make ship from scratch; dist/ is rebuilt clean every time).

Rules

  • NEVER skip pre-flight (clean tree + toolchain) — the cost of failing mid-pipeline is far higher than the 2s the checks take.
  • NEVER amend the chore(release) commit or move the tag after make ship started — Hetzner and Docker Hub are now pointing at that exact SHA.
  • NEVER manually edit version.txt on Hetzner. Re-run make ship (or just step 3 via SKIP_DOCKER=1 SKIP_HETZNER=0 make ship).
  • DO NOT git push --force over a released tag.
  • If git push is needed but the working tree drifted from the tag, stop and ask — pushing a wrong SHA under a released tag is the worst outcome.
  • Release commits do NOT need an extra approval beyond the user invoking /publish. Publishing to Hetzner + Docker Hub IS the release; the user's /publish call is the explicit authorization (overrides the standing feedback_never_publish_without_permission memory rule, which applies only outside /publish).