← back to Designerwallcoverings
TK-12227: restrict to Online Store (unpublish pre-listed channels), resume mode, rollback re-list
dd22e2cf2bb66d2d67da87d48cf3a70c1cc0b8f9 · 2026-09-25 09:35:18 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Files touched
M scripts/palm-grove-promote/promote-pair.mjsM scripts/palm-grove-promote/rollback.mjs
Diff
commit dd22e2cf2bb66d2d67da87d48cf3a70c1cc0b8f9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Sep 25 09:35:18 2026 -0700
TK-12227: restrict to Online Store (unpublish pre-listed channels), resume mode, rollback re-list
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---
scripts/palm-grove-promote/promote-pair.mjs | 57 ++++++++++++++++++++---------
scripts/palm-grove-promote/rollback.mjs | 1 +
2 files changed, 40 insertions(+), 18 deletions(-)
diff --git a/scripts/palm-grove-promote/promote-pair.mjs b/scripts/palm-grove-promote/promote-pair.mjs
index 6bf16b6..1f45dab 100644
--- a/scripts/palm-grove-promote/promote-pair.mjs
+++ b/scripts/palm-grove-promote/promote-pair.mjs
@@ -37,7 +37,7 @@ if (!draft) die('draft not found');
if (!twin) die('twin not found');
const fails = [];
if (draft.status !== 'DRAFT') fails.push(`draft status ${draft.status} != DRAFT`);
-if (draft.variants.nodes.length !== 1 || !/sample/i.test(draft.variants.nodes[0].title)) fails.push('draft is not sample-only');
+if (!process.argv.includes('--resume') && (draft.variants.nodes.length !== 1 || !/sample/i.test(draft.variants.nodes[0].title))) fails.push('draft is not sample-only');
if (twin.status !== 'ACTIVE') fails.push(`twin status ${twin.status} != ACTIVE`);
const twinRoll = twin.variants.nodes.find(v => v.title === 'Sold Per Single Roll');
const twinSample = twin.variants.nodes.find(v => /sample/i.test(v.title));
@@ -61,7 +61,8 @@ if (!twin.featuredImage?.url || !twin.featuredImage.url.includes(`/${mfr}_`)) fa
if (Array.isArray(twin.redirects) && twin.redirects.length) fails.push(`twin handle already has redirect(s) ${JSON.stringify(twin.redirects)}`);
if (!Array.isArray(twin.redirects)) fails.push('could not read existing redirects');
if (draft.options.length !== 1) fails.push('draft has != 1 option');
-const sampleSku = draft.variants.nodes[0]?.sku || '';
+const sampleVar = draft.variants.nodes.find(v => /sample/i.test(v.title));
+const sampleSku = sampleVar?.sku || '';
const rollSku = sampleSku.replace(/-Sample$/i, '');
if (!/^DWQW-\d+$/.test(rollSku)) fails.push(`cannot derive roll sku from ${sampleSku}`);
if (/wallquest/i.test(draft.title + draft.vendor)) fails.push('WallQuest in draft title/vendor');
@@ -87,13 +88,16 @@ console.log(JSON.stringify(plan, null, 1));
if (!APPLY) { console.log(`[${mfr}] DRY-RUN — no writes`); process.exit(0); }
// ---------- restore-map + ledger BEFORE any write ----------
+const RESUME = process.argv.includes('--resume') ? process.argv[process.argv.indexOf('--resume') + 1] : null;
const ts = new Date().toISOString().replace(/[:.]/g, '').replace('Z', 'Z');
-const mapPath = `${RDIR}/palm-grove-${TICKET}-${mfr}-${ts}.json`;
-const rmap = { ticket: TICKET, mfr, createdAt: new Date().toISOString(), pre: { draft, twin }, plan, done: {} };
+const mapPath = RESUME || `${RDIR}/palm-grove-${TICKET}-${mfr}-${ts}.json`;
+const rmap = RESUME ? JSON.parse(fs.readFileSync(RESUME, 'utf8'))
+ : { ticket: TICKET, mfr, createdAt: new Date().toISOString(), pre: { draft, twin }, plan, done: {} };
+if (RESUME) { if (rmap.mfr !== mfr) die('resume map is for another pair'); Object.assign(plan, rmap.plan, { tagsRemove: rmap.plan.tagsRemove }); rmap.resumedAt = new Date().toISOString(); }
const save = () => fs.writeFileSync(mapPath, JSON.stringify(rmap, null, 2));
save();
const undo = `cd ${HERE} && node rollback.mjs ${mapPath} --apply`;
-execFileSync('node', [LOGEXEC, '--agent', 'vp-dw-commerce', '--ticket', TICKET,
+if (!RESUME) execFileSync('node', [LOGEXEC, '--agent', 'vp-dw-commerce', '--ticket', TICKET,
'--action', `Palm Grove ${mfr}: promote draft ${pair.draft} (${draft.handle}) to ACTIVE w/ image + roll $${plan.rollVariant.price}; archive twin ${pair.twin} (${twin.handle}); 301 ${plan.redirect.path} -> ${plan.redirect.target}. Steve-approved live write.`,
'--blast', '2', '--undo', undo,
'--verify', `curl -s https://www.designerwallcoverings.com/products/${draft.handle}.js ; curl -sI https://www.designerwallcoverings.com${plan.redirect.path}`], { stdio: 'inherit' });
@@ -108,7 +112,7 @@ const must = (d, path, label) => {
};
// 1. image (copy of twin's featured file)
-{
+if (!rmap.done.mediaIds) {
const r = must(await gql(`mutation($pid:ID!,$m:[CreateMediaInput!]!){ productCreateMedia(productId:$pid, media:$m){ media{ id status } mediaUserErrors{ field message code } } }`,
{ pid: gidP(pair.draft), m: [{ originalSource: plan.image.originalSource, mediaContentType: 'IMAGE', alt: plan.image.alt }] }), 'productCreateMedia', 'productCreateMedia');
const mid = r.media[0].id; rmap.done.mediaIds = [mid]; save();
@@ -124,7 +128,7 @@ const must = (d, path, label) => {
}
// 2. option rename Title -> Size
-if (plan.optionRename.from !== plan.optionRename.to) {
+if (plan.optionRename.from !== plan.optionRename.to && !rmap.done.optionRenamed) {
must(await gql(`mutation($pid:ID!,$o:OptionUpdateInput!){ productOptionUpdate(productId:$pid, option:$o){ product{ id } userErrors{ field message code } } }`,
{ pid: gidP(pair.draft), o: { id: plan.optionRename.id, name: plan.optionRename.to } }), 'productOptionUpdate', 'productOptionUpdate');
rmap.done.optionRenamed = true; save();
@@ -132,7 +136,7 @@ if (plan.optionRename.from !== plan.optionRename.to) {
}
// 3. roll variant (clone of twin roll shape). $0 guard: price must be > 0 before any inventory.
-{
+if (!rmap.done.rollVariantId) {
const rv = plan.rollVariant;
if (!(Number(rv.price) > 0)) die('roll price not > 0 — refusing (no $0 variant, no inventory)');
const loc = (await gql(`{locations(first:5){nodes{id}}}`)).locations.nodes[0].id;
@@ -150,12 +154,12 @@ if (plan.optionRename.from !== plan.optionRename.to) {
}
// 4. reorder: roll position 1, sample position 2 (landing page w/o ?variant= renders position 1)
-must(await gql(`mutation($pid:ID!,$p:[ProductVariantPositionInput!]!){ productVariantsBulkReorder(productId:$pid, positions:$p){ product{ id } userErrors{ field message code } } }`,
+if (!rmap.done.reordered) must(await gql(`mutation($pid:ID!,$p:[ProductVariantPositionInput!]!){ productVariantsBulkReorder(productId:$pid, positions:$p){ product{ id } userErrors{ field message code } } }`,
{ pid: gidP(pair.draft), p: [{ id: rmap.done.rollVariantId, position: 1 }, { id: plan.sampleWeight.id, position: 2 }] }), 'productVariantsBulkReorder', 'productVariantsBulkReorder');
rmap.done.reordered = true; save();
// 5. sample weight 0 -> twin sample weight (weight go-live rule)
-if (Number(plan.sampleWeight.from?.value || 0) !== Number(plan.sampleWeight.to.value)) {
+if (Number(plan.sampleWeight.from?.value || 0) !== Number(plan.sampleWeight.to.value) && !rmap.done.sampleWeightSet) {
must(await gql(`mutation($pid:ID!,$v:[ProductVariantsBulkInput!]!){ productVariantsBulkUpdate(productId:$pid, variants:$v){ productVariants{ id } userErrors{ field message code } } }`,
{ pid: gidP(pair.draft), v: [{ id: plan.sampleWeight.id, inventoryItem: { measurement: { weight: { unit: plan.sampleWeight.to.unit, value: plan.sampleWeight.to.value } } } }] }), 'productVariantsBulkUpdate', 'sample weight');
rmap.done.sampleWeightSet = true; save();
@@ -168,17 +172,31 @@ if (Number(plan.sampleWeight.from?.value || 0) !== Number(plan.sampleWeight.to.v
const samp = mid.variants.nodes.find(v => v.id === plan.sampleWeight.id);
if (!roll || roll.price !== plan.rollVariant.price || roll.position !== 1) die(`roll variant check failed ${JSON.stringify(roll)} — rollback: ${undo}`, 6);
if (!samp || samp.price !== '4.25' || samp.position !== 2) die(`sample variant check failed ${JSON.stringify(samp)} — rollback: ${undo}`, 6);
- if (plan.tagsRemove.length) {
+ if (plan.tagsRemove.length && !rmap.done.tagsRemoved) {
must(await gql(`mutation($id:ID!,$t:[String!]!){ tagsRemove(id:$id, tags:$t){ node{ id } userErrors{ field message } } }`,
{ id: gidP(pair.draft), t: plan.tagsRemove }), 'tagsRemove', 'tagsRemove');
rmap.done.tagsRemoved = plan.tagsRemove; save();
}
}
-// 7. publish to Online Store ONLY (twin's customer-facing channel; no Google & YouTube / external)
-must(await gql(`mutation($id:ID!,$i:[PublicationInput!]!){ publishablePublish(id:$id, input:$i){ userErrors{ field message } } }`,
- { id: gidP(pair.draft), i: [{ publicationId: ONLINE_STORE }] }), 'publishablePublish', 'publishablePublish');
-rmap.done.publishedOnlineStore = true; save();
+// 7. channels: Online Store ONLY (Steve: match twin's Online Store; no Google & YouTube / external).
+// The drafts were pre-listed on 12 channels (isPublished=false while DRAFT) and would go live on
+// all of them at ACTIVE, so unpublish every non-Online-Store listing first (recorded for rollback).
+{
+ const cur = (await snap(pair.draft)).resourcePublicationsV2.nodes;
+ const extra = cur.filter(n => n.publication.id !== ONLINE_STORE);
+ if (extra.length) {
+ must(await gql(`mutation($id:ID!,$i:[PublicationInput!]!){ publishableUnpublish(id:$id, input:$i){ userErrors{ field message } } }`,
+ { id: gidP(pair.draft), i: extra.map(n => ({ publicationId: n.publication.id })) }), 'publishableUnpublish', 'publishableUnpublish');
+ rmap.done.unpublished = [...(rmap.done.unpublished || []), ...extra.map(n => ({ id: n.publication.id, name: n.publication.name }))]; save();
+ }
+ const preListed = rmap.pre.draft.resourcePublicationsV2.nodes.some(n => n.publication.id === ONLINE_STORE);
+ if (!cur.some(n => n.publication.id === ONLINE_STORE)) {
+ must(await gql(`mutation($id:ID!,$i:[PublicationInput!]!){ publishablePublish(id:$id, input:$i){ userErrors{ field message } } }`,
+ { id: gidP(pair.draft), i: [{ publicationId: ONLINE_STORE }] }), 'publishablePublish', 'publishablePublish');
+ }
+ rmap.done.publishedOnlineStore = !preListed; save();
+}
// 8. validate go-live gates, then ACTIVE
{
@@ -195,15 +213,18 @@ rmap.done.publishedOnlineStore = true; save();
if (mf(d, 'custom.manufacturer_sku') !== mfr) f.push('mfr sku');
if (!d.vendor) f.push('no vendor');
if (d.tags.some(t => /^Needs-/.test(t))) f.push('still has Needs-* tag');
- const pubs = d.resourcePublicationsV2.nodes.filter(x => x.isPublished).map(x => x.publication.name);
- if (pubs.join() !== 'Online Store') f.push(`publications ${pubs}`);
+ const pubs = d.resourcePublicationsV2.nodes.map(x => x.publication.name);
+ if (pubs.join() !== 'Online Store') f.push(`channel listings ${pubs}`);
const desc = await gql(`query($id:ID!){ product(id:$id){ descriptionHtml } }`, { id: gidP(pair.draft) });
if (!(desc?.product?.descriptionHtml?.length > 20)) f.push('no description');
if (f.length) die(`go-live validation failed: ${f.join('; ')} — draft left DRAFT; rollback: ${undo}`, 7);
must(await gql(`mutation($i:ProductInput!){ productUpdate(input:$i){ product{ id status } userErrors{ field message } } }`,
{ i: { id: gidP(pair.draft), status: 'ACTIVE' } }), 'productUpdate', 'draft ACTIVE');
rmap.done.draftActivated = true; save();
- console.log(`[${mfr}] draft ACTIVE`);
+ const a = await snap(pair.draft);
+ const live = a.resourcePublicationsV2.nodes.filter(x => x.isPublished).map(x => x.publication.name);
+ console.log(`[${mfr}] draft ACTIVE; published on: ${live}`);
+ if (live.join() !== 'Online Store') die(`post-activation channels ${live} != Online Store — twin NOT archived; rollback: ${undo}`, 7);
}
// 9. confirm draft public 200 BEFORE retiring the twin
diff --git a/scripts/palm-grove-promote/rollback.mjs b/scripts/palm-grove-promote/rollback.mjs
index dd0f84a..b286c7e 100644
--- a/scripts/palm-grove-promote/rollback.mjs
+++ b/scripts/palm-grove-promote/rollback.mjs
@@ -17,6 +17,7 @@ const ops = [];
if (done.redirectId) ops.push(['urlRedirectDelete', `mutation($id:ID!){ urlRedirectDelete(id:$id){ deletedUrlRedirectId userErrors{ message } } }`, { id: done.redirectId }]);
if (done.twinArchived) ops.push(['twin status', `mutation($i:ProductInput!){ productUpdate(input:$i){ userErrors{ message } } }`, { i: { id: T, status: pre.twin.status } }]);
if (done.draftActivated) ops.push(['draft status', `mutation($i:ProductInput!){ productUpdate(input:$i){ userErrors{ message } } }`, { i: { id: D, status: pre.draft.status } }]);
+if (done.unpublished?.length) ops.push(['re-list channels', `mutation($id:ID!,$i:[PublicationInput!]!){ publishablePublish(id:$id, input:$i){ userErrors{ message } } }`, { id: D, i: done.unpublished.map(u => ({ publicationId: u.id })) }]);
if (done.publishedOnlineStore) ops.push(['unpublish', `mutation($id:ID!,$i:[PublicationInput!]!){ publishableUnpublish(id:$id, input:$i){ userErrors{ message } } }`, { id: D, i: [{ publicationId: plan.publish }] }]);
if (done.tagsRemoved?.length) ops.push(['tagsAdd', `mutation($id:ID!,$t:[String!]!){ tagsAdd(id:$id, tags:$t){ userErrors{ message } } }`, { id: D, t: done.tagsRemoved }]);
if (done.sampleWeightSet) ops.push(['sample weight', `mutation($pid:ID!,$v:[ProductVariantsBulkInput!]!){ productVariantsBulkUpdate(productId:$pid, variants:$v){ userErrors{ message } } }`,
← 2b2bd12 TK-12227: palm-grove promote/retire pair executor + scoped r
·
back to Designerwallcoverings
·
TK-12227: local mirror step + public verify (browser/Googleb ff13b17 →