Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 42 additions & 13 deletions .claude/commands/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ description: Friday release ritual — create git tag, GitHub release (auto-gene
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.**

Expand Down Expand Up @@ -189,18 +190,45 @@ Run the email body through the `humanizer` skill before previewing:

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?"**

Expand All @@ -210,36 +238,37 @@ Do **not** proceed without explicit yes.

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.
Binary file added .claude/release-assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
99 changes: 99 additions & 0 deletions .claude/release-assets/render-thumbnail.mjs
Original file line number Diff line number Diff line change
@@ -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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');

// 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 changes: 128 additions & 0 deletions .claude/release-assets/thumbnail.template.html
Original file line number Diff line number Diff line change
@@ -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>
Binary file added releases/v1.0.6/thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.