← back to Rebel Walls Push
TK-10029: runnable Scope-B rollback (reverse 1220 relabels from durable map)
12856c3062f340d4649257e1c829ffcf90333ab5 · 2026-09-03 12:28:25 -0700 · Steve Abrams
Reads the committed B rollback map and renames each touched option value from
'Mural (per m²)' back to its recorded old_value. Dry-run default; --apply reverts.
Idempotent; never touches the 280 held roll/bolt or originally-canonical products.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A scripts/rollback-apply-B.js
Diff
commit 12856c3062f340d4649257e1c829ffcf90333ab5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 3 12:28:25 2026 -0700
TK-10029: runnable Scope-B rollback (reverse 1220 relabels from durable map)
Reads the committed B rollback map and renames each touched option value from
'Mural (per m²)' back to its recorded old_value. Dry-run default; --apply reverts.
Idempotent; never touches the 280 held roll/bolt or originally-canonical products.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
scripts/rollback-apply-B.js | 75 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 75 insertions(+)
diff --git a/scripts/rollback-apply-B.js b/scripts/rollback-apply-B.js
new file mode 100644
index 0000000..0e6510a
--- /dev/null
+++ b/scripts/rollback-apply-B.js
@@ -0,0 +1,75 @@
+#!/usr/bin/env node
+/**
+ * TK-10029 Scope-B ROLLBACK — reverse the 1220 unit relabels applied 2026-09-03.
+ * Reads the durable B rollback map and, for each row, renames the option value
+ * currently named `new_value` ("Mural (per m²)") back to its recorded `old_value`
+ * on that exact product+option. Pure string flip — mirrors relabel-units.js's
+ * productOptionUpdate; variant id/sku/price/inventory unchanged.
+ *
+ * Only touches products WE changed (map rows). The 280 held roll/bolt values and
+ * the originally-canonical products (never in the map) are untouched.
+ *
+ * Dry-run by default. Pass --apply to execute. Safe to re-run (idempotent —
+ * a row whose value is already back to old_value is skipped).
+ *
+ * Usage:
+ * node scripts/rollback-apply-B.js --map data/relabel-rollback-map-B-...json
+ * node scripts/rollback-apply-B.js --map <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';
+const TOKEN = (() => {
+ 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 args = process.argv.slice(2);
+const DRY = !args.includes('--apply');
+const MAP = (() => { const i = args.indexOf('--map'); return i >= 0 ? args[i + 1] : null; })();
+if (!MAP) { console.error('--map <rollback-map.json> is REQUIRED'); process.exit(1); }
+const CANONICAL = 'Mural (per m²)';
+
+function gqlOnce(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(new Error(`non-JSON (${res.statusCode}): ${String(d).slice(0, 80)}`)); } }); });
+ req.on('error', reject); req.write(body); req.end();
+ });
+}
+async function gql(query, vars) {
+ let last; for (let a = 1; a <= 6; a++) { try { return await gqlOnce(query, vars); } catch (e) { last = e; if (a < 6) await sleep(400 * 2 ** (a - 1)); } } throw last;
+}
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+(async () => {
+ const raw = JSON.parse(fs.readFileSync(MAP, 'utf8'));
+ const rows = Array.isArray(raw) ? raw : raw.rows;
+ console.log(`[rollback-B] mode=${DRY ? 'DRY' : 'LIVE'} rows=${rows.length} map=${MAP}`);
+ let reverted = 0, skipped = 0, err = 0;
+ for (const r of rows) {
+ const detail = await gql(`query { product(id: "${r.product_id}") { options { id name optionValues { id name } } } }`);
+ const opt = detail.data?.product?.options?.find(o => o.id === r.option_id);
+ if (!opt) { console.log(` ERR no option ${r.option_id} on ${r.product_id}`); err++; continue; }
+ const ov = opt.optionValues.find(v => v.name === CANONICAL);
+ if (!ov) { console.log(` SKIP ${r.title} — no "${CANONICAL}" to revert (already ${JSON.stringify(r.old_value)}?)`); skipped++; continue; }
+ console.log(` REVERT ${r.title}: "${CANONICAL}" → ${JSON.stringify(r.old_value)}`);
+ if (DRY) { reverted++; continue; }
+ const res = await gql(`mutation($productId: ID!, $option: OptionUpdateInput!, $ovs: [OptionValueUpdateInput!]!) {
+ productOptionUpdate(productId: $productId, option: $option, optionValuesToUpdate: $ovs) { product { id } userErrors { field message } } }`,
+ { productId: r.product_id, option: { id: opt.id, name: opt.name }, ovs: [{ id: ov.id, name: r.old_value }] });
+ const e = res.data?.productOptionUpdate?.userErrors;
+ if (e && e.length) { console.log(` ERR ${r.product_id}: ${e.map(x => x.message).join('; ')}`); err++; continue; }
+ reverted++;
+ await sleep(120);
+ }
+ console.log(`[rollback-B] done. reverted=${reverted} skipped=${skipped} err=${err} dry=${DRY}`);
+})();
← c93997d TK-10029: retry gql() on transient Shopify upstream errors (
·
back to Rebel Walls Push
·
auto-data-snapshot: 2026-09-03T12:32:21 (1 data files) — dat 2b3cea0 →