← back to Dw Yolo Loop
artmura-site/deploy-vendor.sh
105 lines
#!/usr/bin/env bash
# Generic DW vendor-landing deploy — any line built with build-line.js (CDN images).
# VENDOR=thibaut DOMAIN=thibaut.designerwallcoverings.com bash deploy-vendor.sh
# Self-contained: ships the template app + the line's data snapshot; pm2 (BUNDLE=1) + nginx + Let's Encrypt.
# For Artmura specifically (local downloaded images) use deploy-landing.sh instead.
set -euo pipefail
KAM="${KAM:-root@45.61.58.125}"
VENDOR="${VENDOR:?set VENDOR=<slug>}"
DOMAIN="${DOMAIN:?set DOMAIN=<sub.designerwallcoverings.com>}"
APP="/root/Projects/${VENDOR}-site"
PM2="${VENDOR}-site"
SITE="$(cd "$(dirname "$0")" && pwd)" # the canonical template app dir (artmura-site)
DATA="$SITE/lines/${VENDOR}.json"
HANDLES="$SITE/lines/${VENDOR}-handles.json"
COLORS="$SITE/lines/${VENDOR}-colors.json" # optional
# TK-11200 — showroom-only guard. Refuse to stand up a public microsite (a discovery
# surface) for a showroom-only line; those stay "addressable but not discoverable".
# Checks the SLUG, the DOMAIN and the snapshot's own vendor field, so a renamed slug
# or a mismatched domain can't slip past. Override: ALLOW_SHOWROOM=1.
node -e '
const s = require(process.argv[4]), fs = require("fs");
const cand = new Set([process.argv[1], process.argv[2].split(".")[0]]);
try { cand.add(JSON.parse(fs.readFileSync(process.argv[3], "utf8")).vendor); } catch {}
for (const c of cand) if (c && !s.assertNotShowroom(c, "deploying a public vendor microsite")) process.exit(2);
' "$VENDOR" "$DOMAIN" "$DATA" "$SITE/showroom.js" || exit 2
[ -f "$DATA" ] || { echo "missing $DATA — run: node build-line.js \"<Vendor>\" first"; exit 1; }
[ -f "$HANDLES" ] || { echo "missing $HANDLES"; exit 1; }
echo "→ resolving port (reuse if $PM2 already deployed, else a free one)"
PORT=$(ssh "$KAM" "
cur=\$(pm2 jlist 2>/dev/null | node -e \"try{const a=JSON.parse(require('fs').readFileSync(0));const p=a.find(x=>x.name==='$PM2');process.stdout.write(String((p&&p.pm2_env&&p.pm2_env.env&&p.pm2_env.env.PORT)||''))}catch(e){}\" 2>/dev/null)
if [ -n \"\$cur\" ]; then echo \"\$cur\"; else
for p in 9941 9942 9943 9944 9945 9946 9947 9948 9949; do ss -ltn | grep -q \":\$p \" || { echo \$p; break; }; done
fi")
[ -z "$PORT" ] && { echo "no free port found"; exit 1; }
echo " using port $PORT"
echo "→ shipping app + line data to $KAM:$APP"
ssh "$KAM" "mkdir -p $APP/_data"
rsync -az --delete --exclude node_modules --exclude _images --exclude _data --exclude lines "$SITE/" "$KAM:$APP/"
rsync -az "$DATA" "$HANDLES" "$KAM:$APP/_data/"
[ -f "$COLORS" ] && rsync -az "$COLORS" "$KAM:$APP/_data/" || true
echo "→ install + pm2 (BUNDLE=1, VENDOR=$VENDOR, port $PORT)"
ssh "$KAM" "cd $APP && npm install --omit=dev --silent; pm2 delete $PM2 2>/dev/null; cd $APP && VENDOR=$VENDOR BUNDLE=1 PORT=$PORT pm2 start server.js --name $PM2 && pm2 save"
# TK-11391 — gate by default on the fleet's own naming convention. An "-internal" host is
# a trade/spec viewer and must sit behind the same Basic Auth as the other ~453 of them; a
# public vendor LANDING (artmura, grasscloth) must not. This template previously emitted NO
# auth_basic at all, which is how 65 "-internal" hosts ended up publicly browsable — including
# phillip-jeffries-internal, a showroom line. Override either way with GATE=1 / GATE=0.
HTPASSWD="${HTPASSWD:-/etc/nginx/.htpasswd-dwvendors}"
REALM="${REALM:-Designer Wallcoverings — Trade Lines (Restricted)}"
case "${GATE:-auto}" in
1) DO_GATE=1 ;;
0) DO_GATE=0 ;;
*) case "$DOMAIN" in *-internal.*) DO_GATE=1 ;; *) DO_GATE=0 ;; esac ;;
esac
if [ "$DO_GATE" = 1 ]; then
ssh "$KAM" "[ -f '$HTPASSWD' ]" || { echo "ABORT: $HTPASSWD missing on $KAM — refusing to deploy an internal host ungated"; exit 1; }
AUTH_BLOCK=" auth_basic \"$REALM\";
auth_basic_user_file $HTPASSWD;
"
echo " gate: ON (auth_basic via $HTPASSWD)"
else
AUTH_BLOCK=""
echo " gate: off (public landing)"
fi
echo "→ nginx vhost"
ssh "$KAM" "cat > /etc/nginx/sites-available/$DOMAIN <<NGINX
server {
listen 80;
server_name $DOMAIN;
location / {
$AUTH_BLOCK proxy_pass http://127.0.0.1:$PORT;
proxy_set_header Host \\\$host;
proxy_set_header X-Real-IP \\\$remote_addr;
proxy_set_header X-Forwarded-For \\\$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \\\$scheme;
}
}
NGINX
ln -sf /etc/nginx/sites-available/$DOMAIN /etc/nginx/sites-enabled/$DOMAIN
nginx -t && systemctl reload nginx"
echo "→ Let's Encrypt"
ssh "$KAM" "certbot --nginx -d $DOMAIN --non-interactive --agree-tos -m steve@designerwallcoverings.com --redirect || echo 'certbot: ensure DNS A record exists first, then re-run certbot'"
echo "→ smoke test"
ssh "$KAM" "curl -s -o /dev/null -w 'local pm2 → %{http_code}\n' http://127.0.0.1:$PORT/api/config"
if [ "$DO_GATE" = 1 ]; then
echo "→ gate check (public edge must be 401, not 200)"
code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 "https://$DOMAIN/" 2>/dev/null)
case "$code" in
401|403) echo " OK — $DOMAIN is gated ($code)" ;;
000) echo " (edge not reachable yet — DNS/cert pending; re-check after certbot)" ;;
*) echo " !! WARNING — $DOMAIN returned $code, expected 401. An internal host is PUBLIC." ;;
esac
fi
echo "DONE. Ensure Cloudflare DNS: A $DOMAIN → 45.61.58.125 (proxied)."