feat(release): render a branded changelog thumbnail in the release ritual
Add a 1200x630 changelog thumbnail as a fourth release artifact, generated from a shared brand template that mirrors the marketing-site hero / OG image: warm cream to lavender wash, ink dot-grid, an Instrument Serif headline with a hand-drawn violet squiggle, a segmented 'CHANGELOG | <version>' sticker badge, and colored ink-bordered theme chips with solid offset shadows. - .claude/release-assets/: render-thumbnail.mjs (Playwright renderer), thumbnail.template.html (brand template), logo.png (TryPost wordmark) - .claude/commands/release.md: new Step 5b renders the thumbnail; it is versioned in the artifacts PR alongside the changelog and email - releases/v1.0.6/thumbnail.png: image for the latest release
This commit is contained in:
parent
501de51327
commit
805a628f66
5 changed files with 269 additions and 13 deletions
|
|
@ -3,13 +3,14 @@
|
|||
allowed-tools: Bash, Write, Read, Skill
|
||||
---
|
||||
|
||||
You are running the Friday release ritual for TryPost. Three artifacts are produced:
|
||||
You are running the Friday release ritual for TryPost. Four artifacts are produced:
|
||||
|
||||
1. A git tag (semver)
|
||||
2. A GitHub release with the **auto-generated** changelog (PR list + authors via GitHub's native generator — flat, technical, for developers)
|
||||
3. A **customer-facing email draft** in Cal.com style (themed prose, end-user voice, no commit/PR references)
|
||||
4. A **changelog thumbnail** (1200×630 PNG) rendered from the email's headline and themes, using the TryPost brand template
|
||||
|
||||
Plus local mirrors in `releases/<version>/`.
|
||||
Plus local mirrors in `releases/<version>/` (changelog, email, and thumbnail), versioned via the artifacts PR.
|
||||
|
||||
**Always confirm with the user before any push/tag/release.**
|
||||
|
||||
|
|
@ -189,18 +190,45 @@ ### Step 5 — Humanize the email prose
|
|||
|
||||
The humanizer skill itself covers all patterns. Trust it.
|
||||
|
||||
### Step 5b — Render the changelog thumbnail
|
||||
|
||||
Derive these inputs from the release. The **headline and the chips play different roles — never make one restate the other**:
|
||||
|
||||
- **version** — always pass the release version (e.g. `v1.0.6`). It is stamped in the badge as a mono segment (`★ CHANGELOG | v1.0.6`) so every release image is consistent. Not optional.
|
||||
- **headline** — a crafted marketing hero line (2-6 words, at most two lines), in the voice of the marketing site's hero (`Run your social media on autopilot`). It sells the *benefit* of the release's biggest wins; it is the loudest thing on the image. Do **NOT** paste the subject line, and do **NOT** just list the theme/chip words — the chips already name the areas, so the headline sits one level above them. Read the email's themes **and** the "New features" bullets, find the strongest story, and write a fresh benefit line. A little rhythm helps (a parallel pair reads well, e.g. `Speak every language, reach every reader`). Sentence case, no version number in the headline itself (it lives in the badge), no period.
|
||||
- ❌ `Your language, mobile, and per-image alt text` (this is just the chip labels)
|
||||
- ✅ `Speak every language, reach every reader` (benefit-driven, distinct from the chips)
|
||||
- **underline** — a short emphasis phrase *inside* the headline (1-3 words) to carry the hand-drawn violet squiggle, usually the last / most important phrase (e.g. `every reader`). Must appear in the headline verbatim. Optional; omit for no squiggle.
|
||||
- **themes** — 2-4 short chip labels naming the concrete areas that shipped, condensed to 1-2 words each (e.g. `Languages`, `Mobile`, `Alt text & previews`). These are secondary supporting labels, rendered smaller than the headline; the template auto-colors them (violet / green / sky / orange / rose, in order). Keep them concrete and distinct from the headline's wording.
|
||||
|
||||
Create the directory and render the thumbnail so the user can preview it before confirming:
|
||||
|
||||
```bash
|
||||
mkdir -p releases/<version>
|
||||
node .claude/release-assets/render-thumbnail.mjs \
|
||||
--version "<version>" \
|
||||
--headline "<headline>" \
|
||||
--underline "<emphasis phrase>" \
|
||||
--themes "<label 1>,<label 2>,<label 3>" \
|
||||
--out releases/<version>/thumbnail.png
|
||||
```
|
||||
|
||||
This uses the shared brand template (`.claude/release-assets/thumbnail.template.html`) + TryPost logo. It mirrors the marketing-site hero / OG image: a warm cream→lavender wash, an ink dot-grid, an Instrument Serif headline with a hand-drawn violet squiggle under the emphasis phrase, an amber "Changelog" sticker badge with the version stamped in mono, and colored ink-bordered theme chips with solid offset shadows. 1200×630. It needs Playwright + chromium (already installed for browser tests). If the render fails, report and stop before tagging.
|
||||
|
||||
### Step 6 — Confirm with the user
|
||||
|
||||
Show:
|
||||
1. **Proposed version** (e.g., `v1.0.9 → v1.1.0` — sequential rollover at 9).
|
||||
2. **Changelog preview** (Step 3 output).
|
||||
3. **Email preview**: subject line + full body (post-humanizer).
|
||||
4. **Files that will be created/pushed**:
|
||||
4. **Thumbnail**: `releases/<version>/thumbnail.png` (already rendered in Step 5b — tell the user they can open it to preview).
|
||||
5. **Files that will be created/pushed**:
|
||||
- Tag `<version>` (pushed to origin)
|
||||
- GitHub release `<version>`
|
||||
- `releases/<version>/changelog.md`
|
||||
- `releases/<version>/email.md`
|
||||
- Branch `chore/release-<version>-artifacts` + a PR versioning the two files above
|
||||
- `releases/<version>/thumbnail.png`
|
||||
- Branch `chore/release-<version>-artifacts` + a PR versioning the three files above
|
||||
|
||||
Then ask: **"Create the tag, publish the release, and open the PR with the artifacts?"**
|
||||
|
||||
|
|
@ -210,36 +238,37 @@ ### Step 7 — Execute
|
|||
|
||||
After confirmation, in this exact order. Steps 4–6 (tag + release) run from `main` so the tag stays on the released `main` commit; steps 7–8 version the artifacts on a separate branch and open a PR.
|
||||
|
||||
1. Create local directory: `mkdir -p releases/<version>`
|
||||
1. Create local directory: `mkdir -p releases/<version>` (already created in Step 5b).
|
||||
2. Write `releases/<version>/changelog.md` with the Step 3 content (raw GitHub markdown).
|
||||
3. Write `releases/<version>/email.md` with frontmatter + humanized body.
|
||||
(`releases/<version>/thumbnail.png` was already rendered in Step 5b.)
|
||||
4. From `main`, create the annotated tag: `git tag -a <version> -m "Release <version>"`
|
||||
5. Push tag: `git push origin <version>`
|
||||
6. Create the GitHub release using the changelog file as body:
|
||||
```bash
|
||||
gh release create <version> --title "<version>" --notes-file releases/<version>/changelog.md
|
||||
```
|
||||
7. Version the artifacts on a branch (the two files carry over from the working tree) and push:
|
||||
7. Version the artifacts on a branch (the three files carry over from the working tree) and push:
|
||||
```bash
|
||||
git checkout -b chore/release-<version>-artifacts
|
||||
git add releases/<version>/changelog.md releases/<version>/email.md
|
||||
git commit -m "chore(release): add <version> changelog + customer email artifacts"
|
||||
git add releases/<version>/changelog.md releases/<version>/email.md releases/<version>/thumbnail.png
|
||||
git commit -m "chore(release): add <version> changelog, customer email, and thumbnail artifacts"
|
||||
git push -u origin chore/release-<version>-artifacts
|
||||
```
|
||||
8. Open the PR against `main` (body: what the two files are + a link to the GitHub release):
|
||||
8. Open the PR against `main` (body: what the three files are + a link to the GitHub release):
|
||||
```bash
|
||||
gh pr create --base main --head chore/release-<version>-artifacts \
|
||||
--title "chore(release): <version> changelog + customer email artifacts" \
|
||||
--body "<short body — the two artifact files + link to the release>"
|
||||
--title "chore(release): <version> changelog, customer email + thumbnail artifacts" \
|
||||
--body "<short body — the three artifact files + link to the release>"
|
||||
```
|
||||
9. Report to the user:
|
||||
- GitHub release URL (from `gh release` output)
|
||||
- PR URL (from `gh pr create` output)
|
||||
- Local paths: `releases/<version>/changelog.md`, `releases/<version>/email.md`
|
||||
- Local paths: `releases/<version>/changelog.md`, `releases/<version>/email.md`, `releases/<version>/thumbnail.png`
|
||||
|
||||
### On failure
|
||||
|
||||
- `git push origin <version>` fails: report the exact error, leave the local tag in place, do not retry destructively.
|
||||
- `gh release create` fails: the tag is already pushed; tell the user they can recreate manually with `gh release create <version> --title "<version>" --notes-file releases/<version>/changelog.md`.
|
||||
- `git push` / `gh pr create` for the artifacts branch fails: the tag and GitHub release are already live; report the error and tell the user they can open the PR manually from `chore/release-<version>-artifacts`.
|
||||
- `Skill` or `Write` failure during artifact prep: report and stop. Do not push the tag without the artifacts being prepared.
|
||||
- `Skill`, `Write`, or thumbnail render failure during artifact prep: report and stop. Do not push the tag without the artifacts being prepared.
|
||||
|
|
|
|||
BIN
.claude/release-assets/logo.png
Normal file
BIN
.claude/release-assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
99
.claude/release-assets/render-thumbnail.mjs
Normal file
99
.claude/release-assets/render-thumbnail.mjs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// Render a release changelog thumbnail (1200x630 PNG) from the shared template.
|
||||
//
|
||||
// Usage:
|
||||
// node .claude/release-assets/render-thumbnail.mjs \
|
||||
// --headline "Your language, mobile, and per-image alt text" \
|
||||
// --underline "alt text" \
|
||||
// --themes "Languages,Mobile,Alt text & previews" \
|
||||
// --out releases/v1.0.6/thumbnail.png
|
||||
//
|
||||
// Optional:
|
||||
// --version "v1.0.6" — stamped in the badge as a mono version segment.
|
||||
// --badge "Changelog" (default) — the amber sticker label, top-right.
|
||||
// --underline "phrase" — a phrase inside the headline to get the hand-drawn
|
||||
// violet squiggle (mirrors the marketing-site hero).
|
||||
//
|
||||
// Playwright is resolved from the repo's node_modules, so the script works
|
||||
// regardless of where it is invoked.
|
||||
|
||||
import { createRequire } from 'module';
|
||||
import { readFileSync, writeFileSync, unlinkSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = join(here, '..', '..');
|
||||
const require = createRequire(join(repoRoot, 'package.json'));
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const args = {};
|
||||
for (let i = 2; i < process.argv.length; i += 2) {
|
||||
args[process.argv[i].replace(/^--/, '')] = process.argv[i + 1];
|
||||
}
|
||||
|
||||
if (!args.headline || !args.out) {
|
||||
console.error('usage: render-thumbnail.mjs --headline "..." --themes "a,b,c" --out path.png [--version "v1.0.6"] [--underline "phrase"] [--badge "Changelog"]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const escapeHtml = (value) =>
|
||||
String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"');
|
||||
|
||||
// Hand-drawn violet squiggle under an emphasis phrase — same path the hero uses.
|
||||
const squiggle = (phrase) =>
|
||||
`<span class="ul">${phrase}<svg class="squiggle" viewBox="0 0 200 12" preserveAspectRatio="none" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" aria-hidden="true"><path d="M 5 6 Q 25 0, 50 6 T 100 6 T 150 6 T 195 6" /></svg></span>`;
|
||||
|
||||
let headlineHtml = escapeHtml(args.headline);
|
||||
if (args.underline) {
|
||||
const escapedPhrase = escapeHtml(args.underline);
|
||||
if (headlineHtml.includes(escapedPhrase)) {
|
||||
headlineHtml = headlineHtml.replace(escapedPhrase, squiggle(escapedPhrase));
|
||||
}
|
||||
}
|
||||
|
||||
const themes = (args.themes ?? '')
|
||||
.split(',')
|
||||
.map((theme) => theme.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const chips = themes
|
||||
.map((theme) => `<span class="chip">${escapeHtml(theme)}</span>`)
|
||||
.join('\n ');
|
||||
|
||||
const star = `<svg class="star" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.9 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l7.1-1.01z" /></svg>`;
|
||||
let badgeInner = `<span class="label">${star}${escapeHtml(args.badge ?? 'Changelog')}</span>`;
|
||||
if (args.version) {
|
||||
badgeInner += `<span class="ver">${escapeHtml(args.version)}</span>`;
|
||||
}
|
||||
|
||||
const logoDataUri = `data:image/png;base64,${readFileSync(join(here, 'logo.png')).toString('base64')}`;
|
||||
|
||||
const html = readFileSync(join(here, 'thumbnail.template.html'), 'utf8')
|
||||
.replace('{{LOGO}}', logoDataUri)
|
||||
.replace('{{BADGE_INNER}}', badgeInner)
|
||||
.replace('{{HEADLINE}}', headlineHtml)
|
||||
.replace('{{CHIPS}}', chips);
|
||||
|
||||
const tmpFile = join(tmpdir(), `trypost-thumbnail-${process.pid}.html`);
|
||||
writeFileSync(tmpFile, html);
|
||||
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1200, height: 630 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
await page.goto(`file://${tmpFile}`, { waitUntil: 'networkidle' });
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await page.screenshot({ path: args.out });
|
||||
} finally {
|
||||
await browser.close();
|
||||
unlinkSync(tmpFile);
|
||||
}
|
||||
|
||||
console.log('wrote', args.out);
|
||||
128
.claude/release-assets/thumbnail.template.html
Normal file
128
.claude/release-assets/thumbnail.template.html
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Figtree:wght@300..900&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
/* TryPost brand thumbnail — mirrors the marketing-site hero / OG image:
|
||||
warm cream + ink + signature violet, soft peach→lavender wash, ink
|
||||
dot-grid, Instrument Serif headline with a hand-drawn violet squiggle,
|
||||
and ink-bordered sticker cards with Gumroad-style solid offset shadows.
|
||||
Tokens from trypost-site assets/css/tailwind.css. */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 1200px; height: 630px; }
|
||||
body {
|
||||
font-family: 'Figtree', ui-sans-serif, system-ui, sans-serif;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #faf8f5;
|
||||
color: #0a0a0a;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
/* Broad peach (bottom-left) → lavender (top-right) wash. */
|
||||
.wash {
|
||||
position: absolute; inset: 0; pointer-events: none;
|
||||
background: linear-gradient(118deg,
|
||||
rgba(254,215,170,0.34) 0%,
|
||||
rgba(250,248,245,0) 40%,
|
||||
rgba(250,248,245,0) 58%,
|
||||
rgba(196,181,253,0.46) 100%);
|
||||
}
|
||||
/* Soft glow blobs — violet top-right, warm amber bottom-left. */
|
||||
.blob { position: absolute; border-radius: 999px; pointer-events: none; filter: blur(36px); }
|
||||
.blob-violet { top: -160px; right: -120px; width: 560px; height: 560px;
|
||||
background: radial-gradient(circle, rgba(196,181,253,0.60) 0%, rgba(196,181,253,0) 70%); }
|
||||
.blob-warm { bottom: -190px; left: -150px; width: 540px; height: 540px;
|
||||
background: radial-gradient(circle, rgba(253,215,170,0.55) 0%, rgba(253,215,170,0) 70%); }
|
||||
.blob-fuchsia { bottom: 60px; left: 120px; width: 320px; height: 320px;
|
||||
background: radial-gradient(circle, rgba(245,208,254,0.35) 0%, rgba(245,208,254,0) 70%); }
|
||||
/* Ink dot-grid (over cream, softened by the blobs). */
|
||||
.grid {
|
||||
position: absolute; inset: 0; pointer-events: none; opacity: 0.07;
|
||||
background-image: radial-gradient(circle, #0a0a0a 1px, transparent 1px);
|
||||
background-size: 28px 28px;
|
||||
}
|
||||
.frame {
|
||||
position: relative; z-index: 1;
|
||||
height: 100%; padding: 74px 84px;
|
||||
display: flex; flex-direction: column; justify-content: space-between;
|
||||
}
|
||||
.top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.logo { height: 36px; width: auto; }
|
||||
/* "Changelog" sticker — segmented ticket: amber label + mono version, one
|
||||
ink-bordered pill with an offset shadow (mirrors the site's gold
|
||||
"OPEN SOURCE" sticker). The version is always stamped for consistency. */
|
||||
.badge {
|
||||
display: inline-flex; align-items: stretch;
|
||||
border: 2.5px solid #0a0a0a; border-radius: 10px;
|
||||
box-shadow: 4px 4px 0 0 #0a0a0a; overflow: hidden;
|
||||
}
|
||||
.badge .label {
|
||||
display: inline-flex; align-items: center; gap: 9px;
|
||||
background: #fde68a; color: #0a0a0a;
|
||||
padding: 10px 17px;
|
||||
font-size: 15px; font-weight: 900;
|
||||
text-transform: uppercase; letter-spacing: 0.16em;
|
||||
}
|
||||
.badge .star { width: 16px; height: 16px; display: block; }
|
||||
.badge .ver {
|
||||
display: inline-flex; align-items: center;
|
||||
background: #ffffff; color: #0a0a0a;
|
||||
padding: 10px 15px;
|
||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||
font-size: 15px; font-weight: 700; letter-spacing: 0.01em;
|
||||
border-left: 2.5px solid #0a0a0a;
|
||||
}
|
||||
.body { max-width: 1000px; }
|
||||
h1 {
|
||||
font-family: 'Instrument Serif', Georgia, serif;
|
||||
font-weight: 400; font-size: 88px; line-height: 1.04;
|
||||
letter-spacing: -0.02em; color: #0a0a0a; text-wrap: balance;
|
||||
}
|
||||
/* Emphasis phrase carrying the hand-drawn squiggle. */
|
||||
.ul { position: relative; display: inline-block; white-space: nowrap; }
|
||||
.ul > .squiggle {
|
||||
position: absolute; left: 0; right: 0; bottom: -0.16em;
|
||||
width: 100%; height: 0.34em; color: #8b5cf6;
|
||||
}
|
||||
.themes { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; }
|
||||
/* Theme chips — colored sticker cards (brand pastels), ink border, offset
|
||||
shadow, ink text, hand-placed tilt (echoes the floating platform / composer
|
||||
stickers in the hero — bg-violet-300 ... text-foreground). Secondary to the
|
||||
headline: smaller and lighter in weight. */
|
||||
.chip {
|
||||
padding: 10px 18px; border-radius: 10px;
|
||||
color: #0a0a0a;
|
||||
border: 2.5px solid #0a0a0a;
|
||||
box-shadow: 3px 3px 0 0 #0a0a0a;
|
||||
font-size: 18px; font-weight: 600;
|
||||
}
|
||||
.chip:nth-child(1) { background: #ddd6fe; transform: rotate(-2deg); } /* violet-200 */
|
||||
.chip:nth-child(2) { background: #bbf7d0; transform: rotate(1.5deg); } /* green-200 */
|
||||
.chip:nth-child(3) { background: #bae6fd; transform: rotate(-1deg); } /* sky-200 */
|
||||
.chip:nth-child(4) { background: #fed7aa; transform: rotate(2deg); } /* orange-200 */
|
||||
.chip:nth-child(5) { background: #fecdd3; transform: rotate(-1.5deg); }/* rose-200 */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wash"></div>
|
||||
<div class="blob blob-violet"></div>
|
||||
<div class="blob blob-warm"></div>
|
||||
<div class="blob blob-fuchsia"></div>
|
||||
<div class="grid"></div>
|
||||
<div class="frame">
|
||||
<div class="top">
|
||||
<img class="logo" src="{{LOGO}}" alt="TryPost" />
|
||||
<span class="badge">{{BADGE_INNER}}</span>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<h1>{{HEADLINE}}</h1>
|
||||
</div>
|
||||
|
||||
<div class="themes">{{CHIPS}}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
releases/v1.0.6/thumbnail.png
Normal file
BIN
releases/v1.0.6/thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Loading…
Reference in a new issue