chore: add scripts/release.sh for cutting + publishing releases

Automates what was previously a six-step manual process that, twice
in this codebase's history, has produced version skew between git and
the released tarball (v1.2.0 was published with package.json 1.2.0 in
the tarball but the bump was never committed back to gitea — making
"what code is in v1.2.0?" answerable only by extracting the tarball).

The script:
- Refuses to run with a dirty tree, off main, or already at the
  target version.
- Bumps dashcaddy-api/package.json, rebuilds status/dist/, commits
  + pushes to gitea — so the released artifact and gitea HEAD are
  always in lockstep.
- Clones gitea HEAD on the release host, verifies the cloned commit
  matches what we just pushed (catches a stale clone or a missed
  push), tars it, computes sha256, writes version.json.
- Refreshes install.sh on the release host alongside the tarball
  (fresh installs use the install.sh from the latest release).
- Mirrors the release dir to the get2 backup via rsync.
- Verifies live by curling version.json and re-hashing the served
  tarball.

Hosts overridable via DASHCADDY_RELEASE_HOST / DASHCADDY_MIRROR_HOST
/ DASHCADDY_GITEA_URL env vars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 20:28:45 -07:00
parent d81d1183db
commit 54196a2d4f

133
scripts/release.sh Normal file
View File

@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# Cut a DashCaddy release: bump dashcaddy-api/package.json, commit, push,
# build tarball + version.json on DNS2 (the get.dashcaddy.net publishing
# host), refresh install.sh, then mirror everything to the dc-contabo-de
# get2 backup.
#
# Usage: scripts/release.sh <version>
# Example: scripts/release.sh 1.4.0
#
# Pre-flight: must be on `main`, working tree clean, gitea remote reachable.
#
# Hosts/URLs are overridable via env:
# DASHCADDY_RELEASE_HOST default root@100.104.4.5 (DNS2, hosts get.dashcaddy.net)
# DASHCADDY_MIRROR_HOST default root@dc-contabo-de (hosts get2.dashcaddy.net)
# DASHCADDY_GITEA_URL default http://100.98.123.59:3000/sami7777/dashcaddy.git
set -euo pipefail
VERSION="${1:-}"
[[ -z "$VERSION" ]] && { echo "Usage: $0 <version>" >&2; exit 1; }
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "Invalid version (need X.Y.Z): $VERSION" >&2; exit 1; }
REPO_ROOT="$(git rev-parse --show-toplevel)"
RELEASE_HOST="${DASHCADDY_RELEASE_HOST:-root@100.104.4.5}"
MIRROR_HOST="${DASHCADDY_MIRROR_HOST:-root@dc-contabo-de}"
GITEA_URL="${DASHCADDY_GITEA_URL:-http://100.98.123.59:3000/sami7777/dashcaddy.git}"
cd "$REPO_ROOT"
# ── Pre-flight ─────────────────────────────────────────────────────────────
[[ -f dashcaddy-api/package.json ]] || { echo "Run from dashcaddy repo root (no dashcaddy-api/package.json)" >&2; exit 1; }
[[ -n "$(git status --porcelain)" ]] && { echo "Working tree must be clean" >&2; exit 1; }
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
[[ "$BRANCH" == "main" ]] || { echo "Must be on main (current: $BRANCH)" >&2; exit 1; }
CURRENT="$(node -p "require('./dashcaddy-api/package.json').version")"
[[ "$CURRENT" == "$VERSION" ]] && { echo "package.json already at $VERSION — nothing to do" >&2; exit 1; }
echo "─── Cutting release ───"
echo " current: $CURRENT"
echo " target: $VERSION"
echo " release: $RELEASE_HOST"
echo " mirror: $MIRROR_HOST"
echo
# ── 1. Bump dashcaddy-api/package.json ────────────────────────────────────
echo "[1/6] Bumping dashcaddy-api/package.json"
node -e "
const fs = require('fs');
const pkg = require('./dashcaddy-api/package.json');
pkg.version = '$VERSION';
fs.writeFileSync('./dashcaddy-api/package.json', JSON.stringify(pkg, null, 2) + '\n');
"
# ── 2. Rebuild status frontend so dist/*.js matches source ────────────────
if [[ -f status/build.js ]]; then
echo "[2/6] Rebuilding status frontend"
(cd status && node build.js >/dev/null)
fi
# ── 3. Commit + push ──────────────────────────────────────────────────────
echo "[3/6] Committing + pushing"
git add dashcaddy-api/package.json
# status/dist/ is .gitignored but its tracked files still need re-staging on rebuild.
# -f bypasses the ignore for these specific paths.
[[ -d status/dist ]] && git add -f status/dist/ 2>/dev/null || true
git commit -m "chore(release): bump to $VERSION" >/dev/null
git push gitea main >/dev/null
COMMIT="$(git rev-parse --short HEAD)"
echo " → committed: $COMMIT"
# ── 4. Build tarball on the publishing host ───────────────────────────────
echo "[4/6] Building tarball on $RELEASE_HOST"
ssh "$RELEASE_HOST" "set -e
rm -rf /tmp/dashcaddy-build
mkdir -p /tmp/dashcaddy-build
cd /tmp/dashcaddy-build
git clone --depth 1 '$GITEA_URL' dashcaddy >/dev/null 2>&1
cd dashcaddy
ACTUAL_COMMIT=\$(git rev-parse --short HEAD)
if [ \"\$ACTUAL_COMMIT\" != \"$COMMIT\" ]; then
echo \" ! cloned commit \$ACTUAL_COMMIT does not match expected $COMMIT — aborting\" >&2
exit 1
fi
rm -rf .git
find . -type d -name node_modules -exec rm -rf {} + 2>/dev/null || true
cd /tmp/dashcaddy-build
tar zcf dashcaddy-$VERSION.tar.gz dashcaddy/
"
echo " → built /tmp/dashcaddy-build/dashcaddy-$VERSION.tar.gz"
# ── 5. Publish on the release host ────────────────────────────────────────
echo "[5/6] Publishing v$VERSION + refreshed install.sh on $RELEASE_HOST"
ssh "$RELEASE_HOST" "set -e
cd /var/www/get.dashcaddy.net
cp -a release release.backup-\$(date -u +%Y%m%d-%H%M%S)
cp /tmp/dashcaddy-build/dashcaddy-$VERSION.tar.gz release/
cp /tmp/dashcaddy-build/dashcaddy-$VERSION.tar.gz release/latest.tar.gz
( cd release && sha256sum latest.tar.gz > latest.tar.gz.sha256 )
cp /tmp/dashcaddy-build/dashcaddy/dashcaddy-installer/install.sh release/install.sh
chmod +x release/install.sh
SHA256=\$(sha256sum release/dashcaddy-$VERSION.tar.gz | cut -d' ' -f1)
cat > release/version.json <<EOF
{
\"version\": \"$VERSION\",
\"commit\": \"$COMMIT\",
\"date\": \"\$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
\"sha256\": \"\$SHA256\",
\"changelog\": \"$COMMIT chore(release): bump to $VERSION\",
\"breaking\": false,
\"tarball\": \"dashcaddy-$VERSION.tar.gz\"
}
EOF
"
# ── 6. Mirror to backup host ──────────────────────────────────────────────
echo "[6/6] Mirroring to $MIRROR_HOST"
ssh "$RELEASE_HOST" "rsync -aq --delete /var/www/get.dashcaddy.net/release/ $MIRROR_HOST:/var/www/get2.dashcaddy.net/release/"
# ── Verify ───────────────────────────────────────────────────────────────
echo
echo "─── Verifying live ───"
SERVED_VER="$(curl -fsSL --max-time 5 https://get.dashcaddy.net/release/version.json | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin')).version")"
[[ "$SERVED_VER" == "$VERSION" ]] || { echo "MISMATCH: get.dashcaddy.net serves $SERVED_VER, expected $VERSION" >&2; exit 1; }
echo " get.dashcaddy.net → $SERVED_VER"
SHA_LOCAL="$(ssh "$RELEASE_HOST" "sha256sum /var/www/get.dashcaddy.net/release/dashcaddy-$VERSION.tar.gz | cut -d' ' -f1")"
SHA_HTTP="$(curl -fsSL --max-time 30 "https://get.dashcaddy.net/release/dashcaddy-$VERSION.tar.gz" | sha256sum | cut -d' ' -f1)"
[[ "$SHA_LOCAL" == "$SHA_HTTP" ]] || { echo "SHA mismatch on served tarball" >&2; exit 1; }
echo " tarball sha256 → $SHA_HTTP"
echo
echo "Done. v$VERSION published from commit $COMMIT."