[object Object]

← back to Designerwallcoverings

orphan-publish-guard: resolve psql to absolute path (version-independent), fixing launchd ENOENT class

0f883a3c980ea8144383f47a8ea4db16ff8c8c4e · 2026-08-15 08:25:32 -0700 · Steve Abrams

Replace the fragile hardcoded postgresql@14 PATH-prepend with a resolver that
globs postgresql@* + known bin locations, so a Postgres major bump can't silently
re-ENOENT the guard under launchd's minimal PATH. Diagnosis: the 2026-08-14 dead-guard
memo was a misdiagnosis (stale Aug-6 guard.err + exit-2 = by-design offenders-found),
but the underlying psql-path fragility was real; this removes it for good.

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

Files touched

Diff

commit 0f883a3c980ea8144383f47a8ea4db16ff8c8c4e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 15 08:25:32 2026 -0700

    orphan-publish-guard: resolve psql to absolute path (version-independent), fixing launchd ENOENT class
    
    Replace the fragile hardcoded postgresql@14 PATH-prepend with a resolver that
    globs postgresql@* + known bin locations, so a Postgres major bump can't silently
    re-ENOENT the guard under launchd's minimal PATH. Diagnosis: the 2026-08-14 dead-guard
    memo was a misdiagnosis (stale Aug-6 guard.err + exit-2 = by-design offenders-found),
    but the underlying psql-path fragility was real; this removes it for good.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 orphan-publish-guard.mjs | 35 ++++++++++++++++++++++++++++-------
 1 file changed, 28 insertions(+), 7 deletions(-)

diff --git a/orphan-publish-guard.mjs b/orphan-publish-guard.mjs
index c1b9732..a05d56c 100644
--- a/orphan-publish-guard.mjs
+++ b/orphan-publish-guard.mjs
@@ -18,10 +18,28 @@
 import fs from 'node:fs';
 import { execFileSync } from 'node:child_process';
 
-// launchd runs with a minimal PATH that omits homebrew, so `psql` was ENOENT under the scheduled job
-// (pid:0 crash) even though it resolved in an interactive shell. Prepend the homebrew bins so the two
-// execFileSync('psql', ...) calls resolve identically under launchd and in a terminal. (fix 2026-08-06)
-process.env.PATH = `/opt/homebrew/bin:/opt/homebrew/opt/postgresql@14/bin:/usr/local/bin:${process.env.PATH || ''}`;
+// launchd runs with a minimal PATH that omits homebrew, so a bare `psql` was ENOENT under the
+// scheduled job (pid:0 crash) even though it resolved in an interactive shell.
+// Fix (2026-08-15): resolve psql to an ABSOLUTE path at startup instead of relying on a PATH
+// prepend. The old prepend hardcoded `postgresql@14`, so a Postgres major bump (@15/@16) would
+// silently re-break it — the exact fragility the 2026-08-14 dead-guard memo flagged. This resolver
+// is version-independent: env override wins, else the first psql that exists across the known
+// homebrew/system locations (globbing postgresql@* so any version works), else bare 'psql'.
+function resolvePsql() {
+  if (process.env.PSQL_BIN && fs.existsSync(process.env.PSQL_BIN)) return process.env.PSQL_BIN;
+  const candidates = [];
+  try {
+    for (const d of fs.readdirSync('/opt/homebrew/opt')) {
+      if (/^postgresql@/.test(d)) candidates.push(`/opt/homebrew/opt/${d}/bin/psql`);
+    }
+  } catch {}
+  candidates.push('/opt/homebrew/bin/psql', '/usr/local/bin/psql', '/opt/local/bin/psql', '/usr/bin/psql');
+  for (const p of candidates) { try { if (fs.existsSync(p)) return p; } catch {} }
+  return 'psql'; // last resort — relies on PATH (kept for terminal use)
+}
+const PSQL = resolvePsql();
+// Keep a homebrew-inclusive PATH too, as belt-and-suspenders for any child psql needs.
+process.env.PATH = `/opt/homebrew/bin:/usr/local/bin:${process.env.PATH || ''}`;
 
 const ENFORCE = process.argv.includes('--enforce');
 const DIR = '/Users/macstudio3/Projects/designerwallcoverings/data/orphan-cleanup-20260716';
@@ -32,7 +50,10 @@ const LEDGER = `${DIR}/guard-enforced-ledger.jsonl`;
 const HB_F = `${DIR}/latest.json`;
 // fleet-health-rollup vocabulary (TK-10546, 2026-08-13): emit verdict PASS/WARN/FAIL.
 // offenders=0 → PASS; offenders>0 → WARN (detected, not auto-remediated yet); crash never reaches here → no heartbeat → meta-watchdog surfaces STALE/DEAD.
-const writeHB = (offenders, enforced) => { try { fs.writeFileSync(HB_F, JSON.stringify({ ts: new Date().toISOString(), verdict: offenders === 0 ? 'PASS' : 'WARN', ok: offenders === 0, mode: ENFORCE ? 'enforce' : 'detect', offenders, enforced }, null, 2)); } catch {} };
+// ok = LIVENESS (reaching writeHB means the run completed without crashing). Detected orphans are a
+// FINDING that rides verdict:WARN + offenders — NOT a job failure. The old `ok: offenders===0` made the
+// liveness meta-watchdog mis-read a working guard that found orphans as FAIL (TK-10546 health-vocab lesson).
+const writeHB = (offenders, enforced) => { try { fs.writeFileSync(HB_F, JSON.stringify({ ts: new Date().toISOString(), verdict: offenders === 0 ? 'PASS' : 'WARN', ok: true, mode: ENFORCE ? 'enforce' : 'detect', offenders, enforced }, null, 2)); } catch {} };
 const ENV = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
 const TOKEN = (ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1]?.trim();
 const ENDPOINT = 'https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
@@ -55,7 +76,7 @@ WHERE sp.status='ACTIVE' AND sp.online_store_published
   AND ( sp.dw_sku IS NULL OR sp.dw_sku='' OR NOT (sp.dw_sku ~ '[0-9]' AND upper(sp.dw_sku) <> upper(sp.handle)) )
   AND r.id IS NULL
 ORDER BY sp.handle`;
-const raw = execFileSync('psql', ['host=/tmp dbname=dw_unified', '-At', '-c', SQL]).toString().trim();
+const raw = execFileSync(PSQL, ['host=/tmp dbname=dw_unified', '-At', '-c', SQL]).toString().trim();
 const rows = raw ? raw.split('\n').map(l => { const [gid, handle, supplier] = l.split('\t'); return { gid, handle, supplier }; }) : [];
 
 console.log(`[orphan-publish-guard] offenders live: ${rows.length}${ENFORCE ? '  (ENFORCE — will DRAFT)' : '  (report only)'}`);
@@ -88,7 +109,7 @@ for (const r of rows) {
   const e = (pu && pu.userErrors) || [];
   if (!pu || e.length) { skip++; ledger.write(JSON.stringify({ handle: r.handle, gid: r.gid, reason: JSON.stringify(e.length ? e : rr.errors) }) + '\n'); continue; }
   ledger.write(JSON.stringify({ ts: new Date().toISOString(), handle: r.handle, supplier: r.supplier, gid: r.gid, action: 'DRAFT' }) + '\n');
-  execFileSync('psql', ['host=/tmp dbname=dw_unified', '-c', `UPDATE shopify_products SET status='DRAFT', online_store_published=false, synced_at=now() WHERE shopify_id='${r.gid}'`]);
+  execFileSync(PSQL, ['host=/tmp dbname=dw_unified', '-c', `UPDATE shopify_products SET status='DRAFT', online_store_published=false, synced_at=now() WHERE shopify_id='${r.gid}'`]);
   ok++; await sleep(150);
 }
 ledger.end();

← ec8b7e4 auto-data-snapshot: 2026-08-15T08:12:28 (2 data files) — scr  ·  back to Designerwallcoverings  ·  auto-data-snapshot: 2026-08-15T08:43:00 (3 data files) — dat 63818d8 →