← 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}`);