[object Object]

← back to Designerwallcoverings

TK-11307: theme deploy targeted an UNPUBLISHED theme; rollback reverted other tickets

0a6978b4dfe0f75e12bd3097d345cda0a6898eab · 2026-09-10 16:55:48 -0700 · Steve Abrams

Two defects in the gated theme-push script, both of which would have failed
quietly:

1. THEME_ID was hardcoded to 145121607731 'carnegie-color-swatch', which is
   UNPUBLISHED. The published theme is 145556635699 'DW Sample-Shipping DEV'
   (confirmed via themes.json and the live storefront's Shopify.theme). A PUT
   against the hardcoded id is a customer-facing no-op that still reports
   success. The script's own header said "confirm before apply"; nobody did.
   Now resolved at runtime from role=main, failing loud if absent, with a
   --theme override that warns when the target is not published.

2. --rollback restored the *.ORIGINAL.liquid files, a Sep-3 prep-time snapshot
   that predates TK-11186's two pushes and later Newmor work. Restoring it does
   not undo this deploy -- it reverts TK-11186's Phillip Jeffries showroom hide
   (re-exposing a showroom-only vendor) plus live Newmor behavior. Now each
   apply captures a preimage of the current live bytes and rollback restores
   that; with no preimage it ABORTS rather than falling back. The pre-TK-11186
   vanilla path still exists behind an explicit --rollback-to-original.

Also: an abort no longer prints "ROLLBACK complete" and now exits non-zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FAPArHdMKRiNHqJiorUFm

Files touched

Diff

commit 0a6978b4dfe0f75e12bd3097d345cda0a6898eab
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 10 16:55:48 2026 -0700

    TK-11307: theme deploy targeted an UNPUBLISHED theme; rollback reverted other tickets
    
    Two defects in the gated theme-push script, both of which would have failed
    quietly:
    
    1. THEME_ID was hardcoded to 145121607731 'carnegie-color-swatch', which is
       UNPUBLISHED. The published theme is 145556635699 'DW Sample-Shipping DEV'
       (confirmed via themes.json and the live storefront's Shopify.theme). A PUT
       against the hardcoded id is a customer-facing no-op that still reports
       success. The script's own header said "confirm before apply"; nobody did.
       Now resolved at runtime from role=main, failing loud if absent, with a
       --theme override that warns when the target is not published.
    
    2. --rollback restored the *.ORIGINAL.liquid files, a Sep-3 prep-time snapshot
       that predates TK-11186's two pushes and later Newmor work. Restoring it does
       not undo this deploy -- it reverts TK-11186's Phillip Jeffries showroom hide
       (re-exposing a showroom-only vendor) plus live Newmor behavior. Now each
       apply captures a preimage of the current live bytes and rollback restores
       that; with no preimage it ABORTS rather than falling back. The pre-TK-11186
       vanilla path still exists behind an explicit --rollback-to-original.
    
    Also: an abort no longer prints "ROLLBACK complete" and now exits non-zero.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011FAPArHdMKRiNHqJiorUFm
---
 .../theme-deploy/push-theme-assets.mjs             | 84 +++++++++++++++++++---
 1 file changed, 76 insertions(+), 8 deletions(-)

diff --git a/scripts/tk11186-showroom-hide/theme-deploy/push-theme-assets.mjs b/scripts/tk11186-showroom-hide/theme-deploy/push-theme-assets.mjs
index 430ed32..ebf38d6 100644
--- a/scripts/tk11186-showroom-hide/theme-deploy/push-theme-assets.mjs
+++ b/scripts/tk11186-showroom-hide/theme-deploy/push-theme-assets.mjs
@@ -28,7 +28,13 @@ import { execFileSync } from 'node:child_process';
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 const SHOP = 'designer-laboratory-sandbox.myshopify.com';
 const VER = '2024-10';
-const THEME_ID = '145121607731';                 // published (role=main) — confirm before apply
+// THEME ID IS RESOLVED AT RUNTIME, NOT HARDCODED.
+// 145121607731 ('carnegie-color-swatch') was hardcoded here and named in every TK-11307 memo,
+// but it is UNPUBLISHED. A PUT against it is a customer-facing NO-OP that still reports success.
+// The published theme is whichever one has role=main, so ask the API and fail loud if unsure.
+// Override with --theme <id> only when you deliberately want a non-published theme.
+let THEME_ID = null;
+const THEME_ARG = (() => { const i = process.argv.indexOf('--theme'); return i > -1 ? process.argv[i + 1] : null; })();
 const LOG_EXEC = process.env.HOME + '/.claude/yolo-queue/executed-reversible/log-exec.mjs';
 
 const env = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
@@ -38,7 +44,7 @@ if (!FULL) { console.error('no SHOPIFY_FULL_ACCESS_TOKEN in secrets-manager/.env
 const args = process.argv.slice(2);
 const APPLY = args.includes('--apply');
 const ROLLBACK = args.includes('--rollback');
-const BASE = `https://${SHOP}/admin/api/${VER}/themes/${THEME_ID}/assets.json`;
+const BASE = () => `https://${SHOP}/admin/api/${VER}/themes/${THEME_ID}/assets.json`;
 const md5 = s => crypto.createHash('md5').update(s).digest('hex');
 const H = { 'X-Shopify-Access-Token': FULL, 'Content-Type': 'application/json' };
 
@@ -50,24 +56,80 @@ const FILES = [
 ];
 
 async function getAsset(key) {
-  const u = new URL(BASE); u.searchParams.set('asset[key]', key);
+  const u = new URL(BASE()); u.searchParams.set('asset[key]', key);
   const r = await fetch(u, { headers: H });
   if (!r.ok) throw new Error(`GET ${key} HTTP ${r.status}`);
   return (await r.json()).asset;
 }
 async function putAsset(key, value) {
-  const r = await fetch(BASE, { method: 'PUT', headers: H, body: JSON.stringify({ asset: { key, value } }) });
+  const r = await fetch(BASE(), { method: 'PUT', headers: H, body: JSON.stringify({ asset: { key, value } }) });
   const j = await r.json().catch(() => ({}));
   if (!r.ok) throw new Error(`PUT ${key} HTTP ${r.status} ${JSON.stringify(j).slice(0, 200)}`);
   return j.asset;
 }
 function ledger(rec) { try { execFileSync('node', [LOG_EXEC], { input: JSON.stringify(rec) }); } catch (e) { console.error('  [ledger WARN]', e.message); } }
 
+const PRE = path.join(__dirname, 'preimages');
+const ROLLBACK_TO_ORIGINAL = args.includes('--rollback-to-original');
+
+async function resolvePublishedTheme() {
+  const r = await fetch(`https://${SHOP}/admin/api/${VER}/themes.json`, { headers: H });
+  if (!r.ok) throw new Error(`GET themes.json HTTP ${r.status}`);
+  const themes = (await r.json()).themes || [];
+  const main = themes.find(t => t.role === 'main');
+  if (THEME_ARG) {
+    const t = themes.find(t => String(t.id) === String(THEME_ARG));
+    if (!t) throw new Error(`--theme ${THEME_ARG} is not a theme on this shop`);
+    if (t.role !== 'main') console.error(`  [WARN] --theme ${t.id} '${t.name}' role=${t.role} is NOT published; this write will not reach customers.`);
+    return String(t.id);
+  }
+  if (!main) throw new Error('no theme with role=main — refusing to guess which theme is published');
+  console.log(`  resolved published theme: ${main.id} '${main.name}' (role=main)`);
+  return String(main.id);
+}
+
+// Capture the CURRENT live bytes before every PUT. The *.ORIGINAL.liquid files are a
+// Sep-3 prep-time snapshot that predates TK-11186's two pushes AND later Newmor work, so
+// restoring them does not undo this deploy — it reverts other tickets' shipped changes.
+// A rollback must restore what was live immediately BEFORE this apply, so snapshot it here.
+function writePreimage(key, value, runtag) {
+  fs.mkdirSync(PRE, { recursive: true });
+  const f = path.join(PRE, `${runtag}__${key.replace(/[^\w.-]/g, '_')}`);
+  fs.writeFileSync(f, value);
+  return f;
+}
+function newestPreimage(key) {
+  if (!fs.existsSync(PRE)) return null;
+  const suffix = '__' + key.replace(/[^\w.-]/g, '_');
+  const files = fs.readdirSync(PRE).filter(f => f.endsWith(suffix)).sort();
+  return files.length ? path.join(PRE, files[files.length - 1]) : null;
+}
+
+const ABORTED = [];
 const which = ROLLBACK ? 'orig' : 'fix';
+THEME_ID = await resolvePublishedTheme();
+const RUNTAG = new Date().toISOString().replace(/[:.]/g, '-');
 console.log(`TK-11186 STEP E theme deploy — ${ROLLBACK ? 'ROLLBACK' : APPLY ? 'APPLY' : 'DRY-RUN'} → theme ${THEME_ID}`);
+if (ROLLBACK && ROLLBACK_TO_ORIGINAL) {
+  console.error(`  [WARN] --rollback-to-original restores the Sep-3 PRE-TK-11186 vanilla snippets.`);
+  console.error(`         That DISCARDS TK-11186's Phillip Jeffries showroom hide and later Newmor changes.`);
+  console.error(`         For a normal undo of this deploy, drop the flag and use the captured preimage.`);
+}
 
 for (const f of FILES) {
-  const local = fs.readFileSync(path.join(__dirname, f[which]), 'utf8');
+  let localPath = path.join(__dirname, f[which]);
+  if (ROLLBACK && !ROLLBACK_TO_ORIGINAL) {
+    const pre = newestPreimage(f.key);
+    if (!pre) {
+      console.error(`\n  ROLLBACK ABORTED for ${f.key}: no preimage captured for it.`);
+      console.error(`  Refusing to fall back to the Sep-3 *.ORIGINAL.liquid, which would revert other tickets' work.`);
+      console.error(`  If you truly want the pre-TK-11186 vanilla file, re-run with --rollback-to-original.`);
+      process.exitCode = 1; ABORTED.push(f.key); continue;
+    }
+    localPath = pre;
+    console.log(`\n  ${f.key} rollback source: ${path.basename(pre)}`);
+  }
+  const local = fs.readFileSync(localPath, 'utf8');
   const live = await getAsset(f.key);
   const liveSum = md5(live.value), localSum = md5(local);
   console.log(`\n  ${f.key}`);
@@ -75,6 +137,8 @@ for (const f of FILES) {
   console.log(`    -> ${which} md5 ${localSum} (${local.length}b) ${liveSum === localSum ? '(already matches — no-op)' : ''}`);
   if (!APPLY && !ROLLBACK) continue;
   if (liveSum === localSum) { console.log('    skip (identical)'); continue; }
+  const preFile = writePreimage(f.key, live.value, RUNTAG);
+  console.log(`    preimage captured: ${path.basename(preFile)} (md5 ${liveSum})`);
   const res = await putAsset(f.key, local);
   // Verify — prefer the PUT response's OWN checksum (returned by Shopify on the write itself, so it
   // can't race). Fall back to a re-GET with backoff, since an immediate read-after-write GET can hit a
@@ -91,9 +155,13 @@ for (const f of FILES) {
   ledger({ agent: process.env.TK_AGENT || 'vp-dw-commerce', ticket: 'TK-11186',
     action: `STEP E theme asset ${ROLLBACK ? 'ROLLBACK' : 'deploy'} ${f.key} on theme ${THEME_ID} (md5 ${liveSum} -> ${localSum})`,
     blast_radius: 1,
-    undo_cmd: `node ${path.join(__dirname, 'push-theme-assets.mjs')} --rollback`,
+    undo_cmd: `node ${path.join(__dirname, 'push-theme-assets.mjs')} --rollback`  /* restores the preimage captured by THIS run */,
     verify: `GET assets.json?asset[key]=${f.key} checksum == ${ROLLBACK ? 'ORIGINAL' : localSum}` });
   if (!ok) { console.error('    VERIFY FAILED — investigate before continuing'); process.exit(1); }
 }
-if (!APPLY && !ROLLBACK) console.log('\nDRY-RUN only. Re-run with --apply to deploy, or --rollback to restore originals.');
-else console.log(`\n${ROLLBACK ? 'ROLLBACK' : 'DEPLOY'} complete.`);
+if (!APPLY && !ROLLBACK) console.log('\nDRY-RUN only. Re-run with --apply to deploy, or --rollback to restore the captured preimage.');
+else if (ABORTED.length) {
+  // Never print "complete" over an abort — a rollback that did nothing must not read as success.
+  console.error(`\nROLLBACK DID NOT RUN for ${ABORTED.length} asset(s): ${ABORTED.join(', ')}. Nothing was restored.`);
+  process.exitCode = 1;
+} else console.log(`\n${ROLLBACK ? 'ROLLBACK' : 'DEPLOY'} complete.`);

← 5db8255 TK-11307: fix silent-no-op rollback — reconstruct baseline e  ·  back to Designerwallcoverings  ·  Verify Sanderson gallery remediation for TK-11302 8dcc371 →