← 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
M deploy-vendor.shM manifest.json
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 →