← 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
M orphan-publish-guard.mjs
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 →