[object Object]

← back to Wallco Ai

fix(publish-gate): sudo-psql on Linux + image-path fallback — gate falsely failed on prod

cf75a5c255194229ef1f08c774837c6351443ae7 · 2026-05-29 12:46:12 -0700 · Steve Abrams

Two infra false-fails made the publish gate park EVERY design on prod (never a
real quality verdict):
1. psql() ran bare 'psql dw_unified' as the current user. On prod pm2 runs node
   (and the gate) as root, but PG has no root role → empty result → gate
   reported 'design not found' and parked the publish. Now uses
   'sudo -n -u postgres psql' on Linux, matching lib/db.js PSQL_CMD.
2. Image resolution only tried local_path (a Mac2-style /Users/... path that
   doesn't exist on prod) → 'image file missing/unreadable'. Now falls back to
   data/generated/<basename(local_path|image_url)> + quarantine dirs, same chain
   as the by-id handler.

Surfaced when bulk-publishing colorways-imperial-garden-barone: '0 published,
5 parked (gate)' though the designs render fine. No quality logic changed.

Files touched

Diff

commit cf75a5c255194229ef1f08c774837c6351443ae7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri May 29 12:46:12 2026 -0700

    fix(publish-gate): sudo-psql on Linux + image-path fallback — gate falsely failed on prod
    
    Two infra false-fails made the publish gate park EVERY design on prod (never a
    real quality verdict):
    1. psql() ran bare 'psql dw_unified' as the current user. On prod pm2 runs node
       (and the gate) as root, but PG has no root role → empty result → gate
       reported 'design not found' and parked the publish. Now uses
       'sudo -n -u postgres psql' on Linux, matching lib/db.js PSQL_CMD.
    2. Image resolution only tried local_path (a Mac2-style /Users/... path that
       doesn't exist on prod) → 'image file missing/unreadable'. Now falls back to
       data/generated/<basename(local_path|image_url)> + quarantine dirs, same chain
       as the by-id handler.
    
    Surfaced when bulk-publishing colorways-imperial-garden-barone: '0 published,
    5 parked (gate)' though the designs render fine. No quality logic changed.
---
 scripts/publish-gate.py | 42 +++++++++++++++++++++++++++++++++++-------
 1 file changed, 35 insertions(+), 7 deletions(-)

diff --git a/scripts/publish-gate.py b/scripts/publish-gate.py
index 7641f60..bdd5224 100644
--- a/scripts/publish-gate.py
+++ b/scripts/publish-gate.py
@@ -33,22 +33,50 @@ VMODEL = os.environ.get('OLLAMA_VISION_MODEL', 'llava:latest')
 _spec = importlib.util.spec_from_file_location('es', str(ROOT / 'scripts' / 'edges-scan.py'))
 es = importlib.util.module_from_spec(_spec); _spec.loader.exec_module(es)
 
+# Mirror lib/db.js PSQL_CMD: on Linux (Kamatera) pm2 runs node — and thus this
+# gate — as root, but PG has no 'root' role, so a bare `psql dw_unified` returns
+# nothing and the gate falsely reports 'design not found' (parking every publish
+# on prod). sudo to postgres on Linux; bare psql (trust auth) on macOS.
+_PSQL = (['sudo', '-n', '-u', 'postgres', 'psql', 'dw_unified', '-At', '-F', '|']
+         if sys.platform == 'linux'
+         else ['psql', 'dw_unified', '-At', '-F', '|'])
 def psql(q):
-    return subprocess.run(['psql', 'dw_unified', '-At', '-F', '|', '-c', q], capture_output=True, text=True).stdout.strip()
+    return subprocess.run(_PSQL + ['-c', q], capture_output=True, text=True).stdout.strip()
 
-row = psql(f"SELECT COALESCE(local_path,''), COALESCE(kind,''), COALESCE(category,'') FROM all_designs WHERE id={ID};")
+row = psql(f"SELECT COALESCE(local_path,''), COALESCE(kind,''), COALESCE(category,''), COALESCE(image_url,'') FROM all_designs WHERE id={ID};")
 if not row: print(json.dumps({'pass': False, 'reasons': ['design not found']})); sys.exit(1)
-lp, kind, category = (row.split('|', 2) + ['', '', ''])[:3]
+lp, kind, category, image_url = (row.split('|', 3) + ['', '', '', ''])[:4]
 checks, reasons = {}, []
 
 is_mural = kind in ('mural', 'mural_panel')
 
 from PIL import Image
 import numpy as np
-img = None
-if lp and os.path.isfile(lp):
-    try: img = Image.open(lp).convert('RGB')
-    except Exception: img = None
+# Resolve the image with the same fallback chain as the by-id handler: local_path
+# (often a Mac2-style /Users/... path that doesn't exist on prod), then
+# data/generated/<basename> on THIS box, then the quarantine dirs, then the
+# image_url basename. Without this the gate falsely reports 'image file
+# missing/unreadable' on prod even though the PNG is on disk + served by by-id.
+_IMG_DIR = ROOT / 'data' / 'generated'
+_QUAR = [ROOT / 'data' / 'generated_cactus_quarantine', ROOT / 'data' / 'generated_ghost_quarantine']
+def _resolve_image(lp, image_url):
+    cands = []
+    if lp: cands.append(lp)
+    base = os.path.basename(lp) if lp else ''
+    if base:
+        cands.append(str(_IMG_DIR / base))
+        for q in _QUAR: cands.append(str(q / base))
+    if image_url:
+        iub = os.path.basename(image_url)
+        if iub:
+            cands.append(str(_IMG_DIR / iub))
+            for q in _QUAR: cands.append(str(q / iub))
+    for c in cands:
+        if c and os.path.isfile(c):
+            try: return Image.open(c).convert('RGB')
+            except Exception: pass
+    return None
+img = _resolve_image(lp, image_url)
 if img is None:
     print(json.dumps({'pass': False, 'id': ID, 'reasons': ['image file missing/unreadable']})); sys.exit(1)
 W, H = img.size

← 79f45cc Add 6 room-left PDP theme variants V12-V17 (room setting lef  ·  back to Wallco Ai  ·  audit: unpublish 20 more flagged-goods from haiku full-sweep dd42e27 →