#!/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 # 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 " >&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 # Everything the build rewrites must be staged or the tarball ships stale # copies. status/dist/ is .gitignored (-f bypasses); index.html and sw.js are # tracked but get rewritten by build.js (CSP hash + SW cache tag derived from # bundle content). Without staging sw.js, clients keep the old cache name # and never see the new bundles. [[ -d status/dist ]] && git add -f status/dist/ 2>/dev/null || true [[ -f status/index.html ]] && git add status/index.html 2>/dev/null || true [[ -f status/sw.js ]] && git add status/sw.js 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 # Bake the actual commit SHA into the api VERSION file so containers built # from this tarball report a real commit (not the 'dev' placeholder). echo \"$COMMIT\" > dashcaddy-api/VERSION 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 <&1; then echo " → mirrored" else MIRROR_OK=false echo " ! MIRROR FAILED — get2.dashcaddy.net is stale. Primary continues." >&2 fi # ── Optional: notify known instances to update immediately ──────────────── # Set DASHCADDY_NOTIFY_TARGETS="|,|" to push. # If unset, we try the co-located instance at localhost:3001 using the # generated secret at /opt/dashcaddy/updates/notify-secret (silently skipped # if either is missing). if [[ -n "${DASHCADDY_NOTIFY_TARGETS:-}" ]]; then echo "[notify] pushing release to configured targets" IFS=',' read -ra TARGETS <<< "$DASHCADDY_NOTIFY_TARGETS" for t in "${TARGETS[@]}"; do url="${t%%|*}" secret="${t#*|}" [[ "$url" == "$secret" ]] && { echo " ! malformed target ($t) — need url|secret" >&2; continue; } code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 -X POST \ -H "X-DashCaddy-Notify-Secret: $secret" \ -H 'Content-Type: application/json' \ -d "{\"version\":\"$VERSION\",\"commit\":\"$COMMIT\"}" \ "$url" || true) if [[ "$code" =~ ^2[0-9][0-9]$ ]]; then echo " → $url notified (HTTP $code)" else echo " ! $url notify FAILED (HTTP $code)" >&2 fi done else # Co-located default LOCAL_NOTIFY=$(ssh "$RELEASE_HOST" ' if [[ -r /opt/dashcaddy/updates/notify-secret ]] && curl -fsS --max-time 2 http://localhost:3001/api/health >/dev/null 2>&1; then secret=$(cat /opt/dashcaddy/updates/notify-secret) curl -s -o /dev/null -w "%{http_code}" --max-time 5 -X POST \ -H "X-DashCaddy-Notify-Secret: $secret" \ -H "Content-Type: application/json" \ -d "{\"version\":\"'"$VERSION"'\",\"commit\":\"'"$COMMIT"'\"}" \ http://localhost:3001/api/system/update-notify else echo skip fi ' 2>/dev/null || true) case "$LOCAL_NOTIFY" in 2*) echo "[notify] co-located instance on $RELEASE_HOST → HTTP $LOCAL_NOTIFY" ;; skip) ;; # no secret or instance not up — silent *) echo "[notify] co-located instance notify failed (HTTP $LOCAL_NOTIFY)" >&2 ;; esac fi # ── Verify ─────────────────────────────────────────────────────────────── echo echo "─── Verifying live ───" SERVED_VER="$(curl -fsSL --max-time 5 https://get.dashcaddy.net/release/version.json | grep -oE '"version"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1 | sed -E 's/.*"([^"]+)"$/\1/')" [[ "$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 ✓" if [[ "$MIRROR_OK" == "true" ]]; then GET2_VER="$(curl -fsSL --max-time 5 https://get2.dashcaddy.net/release/version.json 2>/dev/null | grep -oE '"version"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1 | sed -E 's/.*"([^"]+)"$/\1/')" [[ -z "$GET2_VER" ]] && GET2_VER=unreachable if [[ "$GET2_VER" == "$VERSION" ]]; then echo " get2.dashcaddy.net → $GET2_VER ✓" else echo " ! get2.dashcaddy.net serves '$GET2_VER' (expected $VERSION) — check Caddy/DNS for get2" >&2 fi fi echo echo "Done. v$VERSION published from commit $COMMIT." [[ "$MIRROR_OK" == "true" ]] || echo "(reminder: mirror to get2 failed — investigate $MIRROR_HOST)"