← back to Twil Golive 2026 07
activate.mjs
100 lines
// TWIL line go-live (Steve-authorized 2026-07-13: "make all twil items live, even if just memo sample").
// For each target: fetch live product → record rollback → status ACTIVE →
// add $4.25 Sample variant if missing → publish to Online Store ONLY
// (Google & YouTube publication deliberately never touched — GMC bleed rule).
// Usage: node activate.mjs [--canary DWPW-430078] [--limit N]
import { readFileSync, appendFileSync } from 'fs';
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
if (!TOKEN) { console.error('SHOPIFY_ADMIN_TOKEN missing'); process.exit(1); }
const ENDPOINT = 'https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-07/graphql.json';
const ONLINE_STORE_PUB = 'gid://shopify/Publication/22208643184';
const ROLLBACK = 'rollback.jsonl';
const LOG = 'run.log';
const args = process.argv.slice(2);
const canary = args.includes('--canary') ? args[args.indexOf('--canary') + 1] : null;
const limit = args.includes('--limit') ? parseInt(args[args.indexOf('--limit') + 1], 10) : Infinity;
async function gql(query, variables) {
for (let attempt = 0; attempt < 5; attempt++) {
const r = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
const j = await r.json();
if (j.errors?.some(e => e.extensions?.code === 'THROTTLED')) {
await new Promise(res => setTimeout(res, 1500)); continue;
}
if (j.errors) throw new Error(JSON.stringify(j.errors));
return j.data;
}
throw new Error('throttled 5x');
}
const rows = readFileSync('targets.psv', 'utf8').trim().split('\n').map(l => {
const [gid, sku, mfr, status, , ...t] = l.split('|');
return { gid, sku, mfr, mirrorStatus: status, title: t.join('|') };
});
let queue = canary ? rows.filter(r => r.sku === canary) : rows;
queue = queue.slice(0, limit);
console.log(`processing ${queue.length} product(s)${canary ? ' (CANARY)' : ''}`);
let ok = 0, failed = 0;
for (const row of queue) {
try {
const d = await gql(`query($id: ID!) {
product(id: $id) {
id status title
options { name position }
variants(first: 50) { nodes { id title price sku } }
}
}`, { id: row.gid });
const p = d.product;
if (!p) { appendFileSync(LOG, `MISSING ${row.sku} ${row.gid}\n`); failed++; continue; }
appendFileSync(ROLLBACK, JSON.stringify({ gid: p.id, sku: row.sku, prevStatus: p.status, at: new Date().toISOString() }) + '\n');
if (p.status !== 'ACTIVE') {
const u = await gql(`mutation($input: ProductInput!) {
productUpdate(input: $input) { product { id status } userErrors { field message } }
}`, { input: { id: p.id, status: 'ACTIVE' } });
const errs = u.productUpdate.userErrors;
if (errs.length) throw new Error('status: ' + JSON.stringify(errs));
}
const hasSample = p.variants.nodes.some(v => /sample/i.test(v.title) || /sample/i.test(v.sku || ''));
if (!hasSample) {
const optName = p.options[0]?.name || 'Title';
const v = await gql(`mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkCreate(productId: $productId, variants: $variants) {
productVariants { id title price } userErrors { field message }
}
}`, { productId: p.id, variants: [{
price: '4.25',
optionValues: [{ optionName: optName, name: 'Sample' }],
inventoryItem: { sku: `${row.sku}-Sample`, tracked: false },
}]});
const errs = v.productVariantsBulkCreate.userErrors;
if (errs.length) appendFileSync(LOG, `SAMPLE-FAIL ${row.sku}: ${JSON.stringify(errs)}\n`);
else appendFileSync(LOG, `SAMPLE-ADDED ${row.sku}\n`);
}
const pub = await gql(`mutation($id: ID!, $input: [PublicationInput!]!) {
publishablePublish(id: $id, input: $input) { userErrors { field message } }
}`, { id: p.id, input: [{ publicationId: ONLINE_STORE_PUB }] });
const perrs = pub.publishablePublish.userErrors;
if (perrs.length) appendFileSync(LOG, `PUBLISH-FAIL ${row.sku}: ${JSON.stringify(perrs)}\n`);
appendFileSync(LOG, `ACTIVE ${row.sku} (was ${p.status})\n`);
ok++;
await new Promise(res => setTimeout(res, 250));
} catch (e) {
appendFileSync(LOG, `ERROR ${row.sku}: ${e.message}\n`);
failed++;
}
}
console.log(`done: ${ok} activated, ${failed} failed — see ${LOG}`);