[object Object]

← back to Dw Vendor Microsites

deploy-vendor: server-verified free-port guard + post-deploy identity check

3649440f3db0c39ff33fd866f48b3f75a8eee885 · 2026-07-13 01:24:39 -0700 · Steve

Two guards born from the catchii port-collision incident (2026-07-13):
1. Pre-deploy: probe the REAL server for the port (manifest is blind to 200+ pm2 procs);
   if bound by a squatter, auto-pick a verified-free port + persist. Frees own proc first
   so redeploys don't churn.
2. Post-deploy: verify live /api/skus total == staging row count; fail loudly (no DEPLOYED
   mark) if the domain is serving another vendor. The old 401/200 smoke passed on collisions.
Validated on catchii: kept 10107, identity OK (670 rows).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 3649440f3db0c39ff33fd866f48b3f75a8eee885
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 13 01:24:39 2026 -0700

    deploy-vendor: server-verified free-port guard + post-deploy identity check
    
    Two guards born from the catchii port-collision incident (2026-07-13):
    1. Pre-deploy: probe the REAL server for the port (manifest is blind to 200+ pm2 procs);
       if bound by a squatter, auto-pick a verified-free port + persist. Frees own proc first
       so redeploys don't churn.
    2. Post-deploy: verify live /api/skus total == staging row count; fail loudly (no DEPLOYED
       mark) if the domain is serving another vendor. The old 401/200 smoke passed on collisions.
    Validated on catchii: kept 10107, identity OK (670 rows).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 deploy-vendor.sh | 29 +++++++++++++++++++++++++++++
 manifest.json    |  2 +-
 2 files changed, 30 insertions(+), 1 deletion(-)

diff --git a/deploy-vendor.sh b/deploy-vendor.sh
index 89ff4ce..9c12d02 100755
--- a/deploy-vendor.sh
+++ b/deploy-vendor.sh
@@ -42,6 +42,22 @@ PM2NAME="${VENDOR}-viewer"
 JSONL_REMOTE="/root/Projects/dw-vendor-microsites/staging/${VENDOR}.jsonl"
 VDIR_REMOTE="/root/Projects/dw-vendor-microsites/vendors/${VENDOR}/viewer"
 
+# server-port guard: the manifest's port map is blind to what's actually bound on Kamatera
+# (200+ pm2 procs). If PORT is already listening, this deploy would fail to bind and nginx
+# would silently proxy this domain to the squatter (catchii->denovowall->disignum, 2026-07-13).
+# Free THIS vendor's own prior binding first (so a redeploy doesn't see its own port as a
+# squatter), then probe the REAL server; if still taken, pick a verified-free port and persist it.
+ssh my-server "pm2 delete ${PM2NAME} >/dev/null 2>&1 || true; sleep 1"
+PORT_STATE="$(ssh my-server "(exec 3<>/dev/tcp/127.0.0.1/${PORT}) 2>/dev/null && echo BUSY || echo FREE")"
+if [ "$PORT_STATE" = "BUSY" ]; then
+  echo "deploy: port ${PORT} already bound on server — selecting a verified-free port..." >&2
+  NEWPORT="$(ssh my-server "for p in \$(seq 10100 10400); do (exec 3<>/dev/tcp/127.0.0.1/\$p) 2>/dev/null || { echo \$p; break; }; done")"
+  [ -z "$NEWPORT" ] && { echo "deploy: no free server port in 10100-10400" >&2; exit 3; }
+  echo "deploy: reassigning ${VENDOR} port ${PORT} -> ${NEWPORT}" >&2
+  PORT="$NEWPORT"
+  manifest_upsert "$VENDOR" "{\"port\":${PORT}}"
+fi
+
 echo "== deploy ${VENDOR} -> https://${DOMAIN} (port ${PORT}, ${HOUSE}) =="
 
 echo "== 1. rsync viewer + staging -> Kamatera =="
@@ -109,6 +125,19 @@ echo "  no-auth (want 401): \$(curl -s -o /dev/null -w '%{http_code}' -k --resol
 echo "  admin  (want 200):  \$(curl -s -o /dev/null -w '%{http_code}' -k -u admin:DW2024! --resolve ${DOMAIN}:443:45.61.58.125 https://${DOMAIN}/)"
 REMOTE
 
+# post-deploy IDENTITY guard: verify the domain actually serves THIS vendor's catalog, not a
+# squatter's. A collided deploy still 401/200s (both apps are gated) but serves the WRONG rows;
+# compare the live row total to the staging line count. Mismatch => fail loudly, don't mark DEPLOYED.
+STAGING_ROWS="$(grep -c . "$REPO/staging/${VENDOR}.jsonl" 2>/dev/null || echo 0)"
+LIVE_TOTAL="$(curl -s --max-time 20 -u admin:DW2024! "https://${DOMAIN}/api/skus?limit=1" \
+  | python3 -c 'import sys,json;print(json.load(sys.stdin).get("total","?"))' 2>/dev/null || echo '?')"
+if [ "$STAGING_ROWS" != "$LIVE_TOTAL" ]; then
+  echo "== IDENTITY MISMATCH: ${DOMAIN} serves ${LIVE_TOTAL} rows but ${VENDOR} staging has ${STAGING_ROWS} —" >&2
+  echo "   likely PORT COLLISION (domain proxying to another vendor). NOT marking DEPLOYED. ==" >&2
+  exit 4
+fi
+echo "== identity OK: ${DOMAIN} serves ${LIVE_TOTAL} rows (matches ${VENDOR} staging) =="
+
 # record deploy_status (local manifest only; the prod write itself is the gated part)
 manifest_upsert "$VENDOR" "{\"deploy_status\":\"DEPLOYED\"}"
 echo "== DONE — https://${DOMAIN}  (admin / DW2024!) =="
diff --git a/manifest.json b/manifest.json
index 079bfe2..ff69c81 100644
--- a/manifest.json
+++ b/manifest.json
@@ -2356,7 +2356,7 @@
       "image_health": 1.0
     }
   },
-  "updated_at": "2026-07-13T01:19:48.624175",
+  "updated_at": "2026-07-13T01:24:25.150258",
   "stats": {
     "total_vendors": 118,
     "buildable": 117,

← 43683d2 auto-save: 2026-07-13T01:21:52 (1 files) — staging/catchii.j  ·  back to Dw Vendor Microsites  ·  gitignore staging backups; untrack auto-committed catchii .b a045dde →