From 856b90e52bedb22501bbf1c03429c36dae1f7a23 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Wed, 29 Apr 2026 09:01:34 -0700 Subject: [PATCH] chore(release): print a tweet-ready string after a successful release Pulls the bold lead text from each
  • ... in the changelog entry as a tweet-grade summary, fits as many bullets as possible under the 280-char limit (first highlight always wins since it's already the most user-facing line), and prints inside a labeled box with a live char count so the user can copy-paste into @impeccable_ai. Adds tweetHeader and tweetCta to each component config (skill / cli / extension); skill uses the npx skills install line, CLI uses npm i -g, extension drops the CTA entirely (link to the release page is enough since the user has to upload to Chrome Web Store separately). Falls back to header + URL only if even the first highlight overflows. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/release.mjs | 63 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/scripts/release.mjs b/scripts/release.mjs index 7d4029d07..5c5828f2f 100755 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -26,6 +26,8 @@ const COMPONENTS = { buildCmd: 'bun run build', artifacts: ['dist/universal.zip'], postReleaseHint: null, + tweetHeader: (v) => `Impeccable v${v} is out.`, + tweetCta: 'Install / update: npx skills add pbakaus/impeccable', }, cli: { manifest: 'package.json', @@ -35,6 +37,8 @@ const COMPONENTS = { buildCmd: null, artifacts: [], postReleaseHint: 'Run `npm publish` next to push the package to the npm registry.', + tweetHeader: (v) => `Impeccable CLI v${v} is out.`, + tweetCta: 'npm i -g impeccable', }, extension: { manifest: 'extension/manifest.json', @@ -44,9 +48,14 @@ const COMPONENTS = { buildCmd: 'bun run build:extension', artifacts: ['dist/extension.zip'], postReleaseHint: 'Upload `dist/extension.zip` to the Chrome Web Store dashboard to publish.', + tweetHeader: (v) => `Impeccable Chrome extension v${v} is out.`, + tweetCta: null, }, }; +const REPO_URL = 'https://github.com/pbakaus/impeccable'; +const TWEET_LIMIT = 280; + const args = process.argv.slice(2); const dryRun = args.includes('--dry-run'); const component = args.find((a) => !a.startsWith('--')); @@ -196,6 +205,60 @@ if (cfg.postReleaseHint) { console.log(`\n→ Next step: ${cfg.postReleaseHint}`); } +const tweet = renderTweet(cfg, version, entryHtml, tag); +console.log(`\n--- Tweet (${tweet.length}/${TWEET_LIMIT} chars) for @impeccable_ai ---`); +console.log(tweet); +console.log('--- end tweet ---'); + +// Pull the bold lead text from each changelog bullet. Each
  • reads +// "Headline. Body...", so the strong text alone is a +// tweet-grade summary. Returns a list ordered by appearance. +function extractHighlights(entryHtml) { + const highlights = []; + const liRe = /
  • ([\s\S]*?)<\/li>/g; + let match; + while ((match = liRe.exec(entryHtml))) { + const strong = match[1].match(/([\s\S]*?)<\/strong>/); + if (!strong) continue; + const text = strong[1] + .replace(/<[^>]+>/g, '') + .replace(/×/g, '×') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/\s+/g, ' ') + .replace(/[.!?]+\s*$/, '') + .trim(); + if (text) highlights.push(text); + } + return highlights; +} + +function renderTweet(cfg, version, entryHtml, tag) { + const releaseUrl = `${REPO_URL}/releases/tag/${tag}`; + const header = cfg.tweetHeader(version); + const highlights = extractHighlights(entryHtml); + const tail = [cfg.tweetCta, releaseUrl].filter(Boolean).join('\n'); + + // Greedy: include as many highlights as fit. Always include the URL. + let bullets = ''; + const bulletPrefix = '• '; + for (const h of highlights) { + const candidate = bullets + bulletPrefix + h + '\n'; + const draft = [header, '', candidate.trimEnd(), '', tail].join('\n'); + if (draft.length > TWEET_LIMIT) break; + bullets = candidate; + } + + // Fallback if even the first highlight overflows: drop bullets entirely. + if (!bullets) { + return [header, '', tail].join('\n'); + } + return [header, '', bullets.trimEnd(), '', tail].join('\n'); +} + function htmlToMarkdown(html) { let md = html; md = md.replace(/
    /, '');