← 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
M scripts/tk11186-showroom-hide/theme-deploy/push-theme-assets.mjs
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 →