#!/usr/bin/env node /** * Generates docs/releases.md from GitHub releases. * * Usage: * node scripts/sync-release-notes.mjs * * Requires: gh CLI authenticated */ import { execFileSync } from 'child_process'; import { writeFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const projectRoot = join(__dirname, '..'); function fetchReleases() { try { const output = execFileSync( 'gh', ['release', 'list', '--limit', '100', '--json', 'tagName,name,publishedAt,isPrerelease'], { encoding: 'utf-8', cwd: projectRoot } ); return JSON.parse(output); } catch (error) { console.error('Failed to fetch releases:', error.message); process.exit(1); } } function fetchReleaseBody(tagName) { try { const output = execFileSync( 'gh', ['release', 'view', tagName, '--json', 'body', '-q', '.body'], { encoding: 'utf-8', cwd: projectRoot } ); return output.trim(); } catch { return ''; } } function formatDate(isoDate) { const date = new Date(isoDate); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); } function getMinorVersion(tagName) { // v0.12.3 -> v0.12 const match = tagName.match(/^v?(\d+)\.(\d+)/); if (match) { return `v${match[1]}.${match[2]}`; } return tagName; } function groupReleasesByMinor(releases) { const groups = new Map(); for (const release of releases) { // Skip pre-releases and RC versions for the main listing if (release.isPrerelease || release.tagName.includes('-rc') || release.tagName.includes('-RC')) { continue; } const minor = getMinorVersion(release.tagName); if (!groups.has(minor)) { groups.set(minor, []); } groups.get(minor).push(release); } // Sort each group by version descending for (const [, releases] of groups) { releases.sort((a, b) => { const aVer = a.tagName.replace(/^v/, '').split('.').map(Number); const bVer = b.tagName.replace(/^v/, '').split('.').map(Number); for (let i = 0; i < 3; i++) { if ((bVer[i] || 0) !== (aVer[i] || 0)) { return (bVer[i] || 0) - (aVer[i] || 0); } } return 0; }); } return groups; } function extractTitle(releaseName) { // "v0.12.3 | Thinking, Spec-Kits, Context Management" -> "Thinking, Spec-Kits, Context Management" const match = releaseName.match(/\|\s*(.+)$/); return match ? match[1].trim() : ''; } function generateMarkdown(releases) { const grouped = groupReleasesByMinor(releases); // Sort groups by version descending const sortedGroups = Array.from(grouped.entries()).sort((a, b) => { const aVer = a[0].replace(/^v/, '').split('.').map(Number); const bVer = b[0].replace(/^v/, '').split('.').map(Number); for (let i = 0; i < 2; i++) { if ((bVer[i] || 0) !== (aVer[i] || 0)) { return (bVer[i] || 0) - (aVer[i] || 0); } } return 0; }); let md = `--- title: Release Notes description: Version history and changelog for Maestro releases --- # Release Notes This page documents the version history of Maestro, including new features, improvements, and bug fixes for each release. Maestro can update itself automatically! This feature was introduced in **v0.8.7** (December 16, 2025). Enable auto-updates in Settings to stay current. --- `; for (const [minor, minorReleases] of sortedGroups) { const latest = minorReleases[0]; const title = extractTitle(latest.name); const titleSuffix = title ? ` - ${title}` : ''; md += `## ${minor}.x${titleSuffix}\n\n`; md += `**Latest: ${latest.tagName}** | Released ${formatDate(latest.publishedAt)}\n\n`; // Fetch and include the release body for the latest in this minor const body = fetchReleaseBody(latest.tagName); if (body) { // Clean up the body - remove "Full Changelog" links as they're redundant let cleanBody = body .replace(/\*\*Full Changelog\*\*:.*$/gm, '') .replace(/## What's Changed[\s\S]*?(?=##|$)/g, '') // Remove "What's Changed" sections .replace(/## New Contributors[\s\S]*?(?=##|$)/g, '') // Remove "New Contributors" sections .trim(); // If body starts with a list, add context if (cleanBody.startsWith('-') || cleanBody.startsWith('*')) { md += '### Changes\n\n'; } md += cleanBody + '\n\n'; } // If there are patch releases, list them briefly if (minorReleases.length > 1) { md += '### Previous Releases in this Series\n\n'; for (let i = 1; i < minorReleases.length; i++) { const rel = minorReleases[i]; const relTitle = extractTitle(rel.name); md += `- **${rel.tagName}** (${formatDate(rel.publishedAt)})`; if (relTitle) { md += ` - ${relTitle}`; } md += '\n'; } md += '\n'; } md += '---\n\n'; } md += `## Downloading Releases All releases are available on the [GitHub Releases page](https://github.com/pedramamini/Maestro/releases). Maestro is available for: - **macOS** - Apple Silicon (arm64) and Intel (x64) - **Windows** - x64 - **Linux** - x64 and arm64, AppImage, deb, and rpm packages `; return md; } async function main() { console.log('Fetching releases from GitHub...'); const releases = fetchReleases(); console.log(`Found ${releases.length} releases`); console.log('Generating markdown...'); const markdown = generateMarkdown(releases); const outputPath = join(projectRoot, 'docs', 'releases.md'); writeFileSync(outputPath, markdown, 'utf-8'); console.log(`Written to ${outputPath}`); } main().catch(console.error);