← back to Rebel Walls Push
TK-10029: one-command rollback executor (reads durable ledger, reverts canonical->old_value)
72b8edd7985aaf8dcebe504b5cea252a8ae1c54a · 2026-09-03 11:09:51 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VA9ekkiMa6vAC2RaDP11zU
Files touched
A scripts/rollback-relabel.js
Diff
commit 72b8edd7985aaf8dcebe504b5cea252a8ae1c54a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 3 11:09:51 2026 -0700
TK-10029: one-command rollback executor (reads durable ledger, reverts canonical->old_value)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VA9ekkiMa6vAC2RaDP11zU
---
scripts/rollback-relabel.js | 65 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 65 insertions(+)
diff --git a/scripts/rollback-relabel.js b/scripts/rollback-relabel.js
new file mode 100644
index 0000000..4322e77
--- /dev/null
+++ b/scripts/rollback-relabel.js
@@ -0,0 +1,65 @@
+#!/usr/bin/env node
+/**
+ * TK-10029 ROLLBACK: reverse the Roll->"Mural (per m²)" relabel using a durable
+ * pre-apply rollback ledger. For each recorded rename, renames the canonical option
+ * value BACK to its captured old_value. Dry-run by default; --apply to execute.
+ *
+ * Usage:
+ * node scripts/rollback-relabel.js --ledger data/relabel-rollback-map-<ISO>.json
+ * node scripts/rollback-relabel.js --ledger <file> --apply
+ */
+const https = require('https');
+const fs = require('fs');
+
+const SECRETS_ENV = '/Users/macstudio3/Projects/secrets-manager/.env';
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const API_VERSION = '2024-10';
+
+function getToken() {
+ const env = fs.readFileSync(SECRETS_ENV, 'utf8');
+ const m = env.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m);
+ if (!m) throw new Error('SHOPIFY_FULL_ACCESS_TOKEN not found');
+ return m[1].trim();
+}
+const TOKEN = getToken();
+const args = process.argv.slice(2);
+const DRY = !args.includes('--apply');
+const CANONICAL = 'Mural (per m²)';
+const li = args.indexOf('--ledger');
+if (li < 0) { console.error('need --ledger <file>'); process.exit(1); }
+const ledger = JSON.parse(fs.readFileSync(args[li + 1], 'utf8'));
+
+function gql(query, vars) {
+ return new Promise((resolve, reject) => {
+ const body = JSON.stringify({ query, variables: vars || {} });
+ const req = https.request({
+ hostname: DOMAIN, path: `/admin/api/${API_VERSION}/graphql.json`, method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
+ }, res => { let d=''; res.on('data',c=>d+=c); res.on('end',()=>{try{resolve(JSON.parse(d))}catch(e){reject(e)}}); });
+ req.on('error', reject); req.write(body); req.end();
+ });
+}
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+(async () => {
+ console.log(`[rollback] mode=${DRY?'DRY-RUN':'LIVE'} rows=${ledger.renames.length}`);
+ let done=0, skip=0, err=0;
+ for (const row of ledger.renames) {
+ const detailQ = `query { product(id: "${row.productId}") { options { id name optionValues { id name } } } }`;
+ const detail = await gql(detailQ);
+ const opt = detail.data?.product?.options?.find(o => o.id === row.optionId);
+ if (!opt) { console.log(` SKIP ${row.title} — option gone`); skip++; continue; }
+ const ov = opt.optionValues.find(v => v.name === CANONICAL);
+ if (!ov) { console.log(` SKIP ${row.title} — canonical not present (already reverted?)`); skip++; continue; }
+ console.log(` REVERT ${row.title}: "${CANONICAL}" -> "${row.old_value}"`);
+ if (DRY) { done++; continue; }
+ const mutation = `mutation($productId: ID!, $option: OptionUpdateInput!, $ovs: [OptionValueUpdateInput!]!) {
+ productOptionUpdate(productId:$productId, option:$option, optionValuesToUpdate:$ovs) { userErrors { message } } }`;
+ const res = await gql(mutation, { productId: row.productId,
+ option: { id: row.optionId, name: row.optionName }, ovs: [{ id: ov.id, name: row.old_value }] });
+ const errs = res.data?.productOptionUpdate?.userErrors;
+ if (errs && errs.length) { console.log(` ERR ${row.title}: ${errs.map(e=>e.message).join('; ')}`); err++; }
+ else { done++; await sleep(500); }
+ }
+ console.log(`[rollback] done=${done} skip=${skip} err=${err} dry=${DRY}`);
+})().catch(e => { console.error('[rollback] FATAL', e.message); process.exit(1); });
← 789f983 TK-10029: fresh complete pre-apply rollback ledger (1500 ren
·
back to Rebel Walls Push
·
TK-10029: durable scope-B rollback map (1220 rows) + generat dd06ad8 →