[object Object]

← back to Greenland Onboard

Greenland cork go-live: publish the 1 active-unpublished SKU (Montauk-Bison Gold)

664f66c8e42774aea72a0e25b8d698803b130a95 · 2026-07-15 14:21:14 -0700 · Steve

Reality check via live Shopify (snapshot was stale): cork line is ALREADY 195/196
active SKUs published. Published the 1 remaining active-unpublished (verified live,
storefront 200). publish-online.mjs guards: Online Store only, Phillipe-Romano-name /
no-Greenland / no-Wallpaper rails, and status===ACTIVE (never resurrect the 15 ARCHIVED
= discontinued SKUs). Canary correctly no-op'd on archived (no harm).

Files touched

Diff

commit 664f66c8e42774aea72a0e25b8d698803b130a95
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 15 14:21:14 2026 -0700

    Greenland cork go-live: publish the 1 active-unpublished SKU (Montauk-Bison Gold)
    
    Reality check via live Shopify (snapshot was stale): cork line is ALREADY 195/196
    active SKUs published. Published the 1 remaining active-unpublished (verified live,
    storefront 200). publish-online.mjs guards: Online Store only, Phillipe-Romano-name /
    no-Greenland / no-Wallpaper rails, and status===ACTIVE (never resurrect the 15 ARCHIVED
    = discontinued SKUs). Canary correctly no-op'd on archived (no harm).
---
 data/published-online.ndjson | 19 +++++++++++
 scripts/publish-online.mjs   | 80 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 99 insertions(+)

diff --git a/data/published-online.ndjson b/data/published-online.ndjson
new file mode 100644
index 0000000..5d8a378
--- /dev/null
+++ b/data/published-online.ndjson
@@ -0,0 +1,19 @@
+{"ts":"2026-07-15T21:15:49.133Z","pid":"7880972369971","title":"Southampton-Sharkskin Cork Wallcovering | Phillipe Romano","handle":"southampton-cork-wallcovering-sharkskin","action":"published"}
+{"ts":"2026-07-15T21:15:50.102Z","pid":"7880972402739","title":"Southampton-Eden Cork Wallcovering | Phillipe Romano","handle":"southampton-cork-wallcovering-eden","action":"published"}
+{"ts":"2026-07-15T21:15:51.006Z","pid":"7880972533811","title":"Southampton-Deep Taupe Cork Wallcovering | Phillipe Romano","handle":"southampton-cork-wallcovering-deep-taupe","action":"published"}
+{"ts":"2026-07-15T21:19:41.359Z","pid":"7880972369971","title":"Southampton-Sharkskin Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:41.553Z","pid":"7880972402739","title":"Southampton-Eden Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:41.754Z","pid":"7880972533811","title":"Southampton-Deep Taupe Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:41.970Z","pid":"7880972468275","title":"Southampton-Coffee Bean Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:42.179Z","pid":"7880972435507","title":"Southampton-Argan Oil Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:42.387Z","pid":"7880972501043","title":"Southampton-Winter Bloom Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:42.588Z","pid":"7880972140595","title":"Palm Beach-White Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:42.814Z","pid":"7880972075059","title":"Palm Beach-Eiffel Tower Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:43.005Z","pid":"7880972173363","title":"Palm Beach-Amphora Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:43.219Z","pid":"7880972206131","title":"Palm Beach-Rhododendron Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:43.436Z","pid":"7880972238899","title":"Palm Beach-Picante Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:43.632Z","pid":"7880972271667","title":"Palm Beach-Taupe Gray Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:43.850Z","pid":"7880972337203","title":"Palm Beach-Java Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:44.064Z","pid":"7880972304435","title":"Palm Beach-Blue  Nights Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:19:44.263Z","pid":"7880972107827","title":"Palm Beach-Turkish  Tile Cork Wallcovering | Phillipe Romano","status":"ARCHIVED","action":"skip-not-active"}
+{"ts":"2026-07-15T21:20:11.024Z","pid":"7880989343795","title":"Montauk-Bison Gold Cork Wallcovering | Phillipe Romano","handle":"montauk-cork-wallcovering-bison-gold","action":"published"}
diff --git a/scripts/publish-online.mjs b/scripts/publish-online.mjs
new file mode 100644
index 0000000..ce5f185
--- /dev/null
+++ b/scripts/publish-online.mjs
@@ -0,0 +1,80 @@
+#!/usr/bin/env node
+// Publish existing (active) Greenland cork products to the ONLINE STORE channel — go-live.
+// Customer-facing. Safe by design:
+//   • publishes ONLY to Online Store (never Google/other channels)
+//   • per-SKU guard: title must contain "Phillipe Romano" and must NOT contain
+//     "Greenland" or "Wallpaper" — else SKIP + flag (private-label + banned-word rails)
+//   • skips products already published to Online Store (idempotent re-run)
+//   • ledger to data/published-online.ndjson; throttled + 429 retry
+//   Usage: node scripts/publish-online.mjs <limit> [startIndex] [--dry]   (limit=0 → all)
+import { readFileSync, appendFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const env = readFileSync(`${process.env.HOME}/Projects/secrets-manager/.env`, 'utf8');
+const pick = (k) => (env.match(new RegExp(`^${k}=(.*)$`, 'm'))?.[1] || '').replace(/^['"]|['"]$/g, '').trim();
+const STORE = pick('SHOPIFY_STORE') || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = pick('SHOPIFY_ADMIN_TOKEN');
+const API = `https://${STORE}/admin/api/2024-10/graphql.json`;
+const ONLINE_STORE = 'gid://shopify/Publication/22208643184';
+const H = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
+const LIMIT = Number(process.argv[2] || 0);
+const START = Number(process.argv[3] || 0);
+const DRY = process.argv.includes('--dry');
+const LEDGER = join(ROOT, 'data', 'published-online.ndjson');
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+
+async function gql(query, variables) {
+  for (let i = 0; i < 5; i++) {
+    const r = await fetch(API, { method: 'POST', headers: H, body: JSON.stringify({ query, variables }) });
+    if (r.status === 429) { await sleep(2000 * (i + 1)); continue; }
+    const j = await r.json();
+    if (j.errors) throw new Error(JSON.stringify(j.errors).slice(0, 300));
+    return j.data;
+  }
+  throw new Error('429 exhausted');
+}
+
+const products = JSON.parse(readFileSync(join(ROOT, 'public', 'viewer.json'), 'utf8')).products
+  .filter(p => p.shopify_pid);                       // must have a real Shopify product id
+const slice = products.slice(START);
+// LIMIT = max REAL publishes (canary), not first-N-products (most are already live)
+console.log(`publish-online: scanning ${slice.length}${LIMIT ? `, stop after ${LIMIT} publish(es)` : ''}${DRY ? ' [DRY]' : ''} → Online Store`);
+
+let published = 0, already = 0, skipped = 0, failed = 0;
+for (const p of slice) {
+  const gid = `gid://shopify/Product/${p.shopify_pid}`;
+  const t = String(p.title || '');
+  // rails
+  if (!/phillipe romano/i.test(t) || /greenland/i.test(t) || /\bwallpaper\b/i.test(t)) {
+    console.log(`  SKIP(rails) ${p.shopify_pid} :: ${t}`); skipped++;
+    appendFileSync(LEDGER, JSON.stringify({ ts: new Date().toISOString(), pid: p.shopify_pid, title: t, action: 'skip-rails' }) + '\n'); continue;
+  }
+  // live state
+  let d;
+  try {
+    d = await gql(`query($id:ID!){ product(id:$id){ id title status handle publishedOnPublication(publicationId:"${ONLINE_STORE}") } }`, { id: gid });
+  } catch (e) { console.log(`  FAIL(read) ${p.shopify_pid} :: ${e.message}`); failed++; continue; }
+  const prod = d.product;
+  if (!prod) { console.log(`  FAIL(missing) ${p.shopify_pid}`); failed++; continue; }
+  if (prod.status !== 'ACTIVE') {   // ARCHIVED = discontinued (hard rule) — NEVER publish/resurrect
+    skipped++;
+    appendFileSync(LEDGER, JSON.stringify({ ts: new Date().toISOString(), pid: p.shopify_pid, title: prod.title, status: prod.status, action: 'skip-not-active' }) + '\n');
+    continue;
+  }
+  if (prod.publishedOnPublication) { already++; continue; }   // idempotent
+  if (DRY) { console.log(`  WOULD publish ${p.shopify_pid} :: ${prod.title}`); published++; continue; }
+  try {
+    const r = await gql(
+      `mutation($id:ID!,$input:[PublicationInput!]!){ publishablePublish(id:$id,input:$input){ userErrors{field message} } }`,
+      { id: gid, input: [{ publicationId: ONLINE_STORE }] });
+    const errs = r.publishablePublish.userErrors;
+    if (errs.length) { console.log(`  ERR ${p.shopify_pid} :: ${JSON.stringify(errs)}`); failed++; continue; }
+    published++; console.log(`  ✓ ${p.shopify_pid} :: ${prod.title}`);
+    appendFileSync(LEDGER, JSON.stringify({ ts: new Date().toISOString(), pid: p.shopify_pid, title: prod.title, handle: prod.handle, action: 'published' }) + '\n');
+  } catch (e) { console.log(`  FAIL(pub) ${p.shopify_pid} :: ${e.message}`); failed++; }
+  await sleep(350);
+  if (LIMIT && published >= LIMIT) break;   // canary cap on REAL publishes
+}
+console.log(`\nDONE — published:${published} already:${already} skipped(rails):${skipped} failed:${failed}`);

← 8e47620 Greenland cork disco: archive 15 Phillipe Romano cork SKUs (  ·  back to Greenland Onboard  ·  greenland-onboard: add Memo/Stock/Price chip actions via sha 33f8806 →