From f6ef0972581e1ea574fefce9dc2d53e1edd9d987 Mon Sep 17 00:00:00 2001 From: Kris Braun Date: Sat, 18 Oct 2025 22:47:02 -0400 Subject: [PATCH] Improve changelog format --- .changeset/changelog-formatter.js | 92 +++++++++++++++ .changeset/config.json | 2 +- .changeset/fruity-horses-grow.md | 8 ++ .changeset/reorganize-changelogs.js | 160 ++++++++++++++++++++++++++ .changeset/validate-changesets.js | 149 ++++++++++++++++++++++++ .github/workflows/changeset-check.yml | 3 + .husky/pre-commit | 4 + package.json | 7 +- pnpm-lock.yaml | 10 ++ sdk/CHANGELOG.md | 21 ++-- tools/google-calendar/CHANGELOG.md | 8 +- tools/google-contacts/CHANGELOG.md | 8 +- tools/outlook-calendar/CHANGELOG.md | 8 +- 13 files changed, 457 insertions(+), 23 deletions(-) create mode 100644 .changeset/changelog-formatter.js create mode 100644 .changeset/fruity-horses-grow.md create mode 100644 .changeset/reorganize-changelogs.js create mode 100644 .changeset/validate-changesets.js create mode 100644 .husky/pre-commit diff --git a/.changeset/changelog-formatter.js b/.changeset/changelog-formatter.js new file mode 100644 index 0000000..6c995f0 --- /dev/null +++ b/.changeset/changelog-formatter.js @@ -0,0 +1,92 @@ +const { getInfo } = require("@changesets/get-github-info"); + +/** + * Custom changelog formatter for Plot project + * Follows "Keep a Changelog" standard with categories: + * Added, Changed, Deprecated, Removed, Fixed, Security + * + * Expects changeset summaries to be prefixed with category, e.g.: + * "Added: new feature" + * "Fixed: bug description" + */ + +const CATEGORIES = [ + "Added", + "Changed", + "Deprecated", + "Removed", + "Fixed", + "Security" +]; + +const CATEGORY_PATTERN = new RegExp(`^(${CATEGORIES.join("|")}):\\s*`, "i"); + +/** + * Parse a changeset summary to extract category and content + */ +function parseChangeset(summary) { + const match = summary.match(CATEGORY_PATTERN); + + if (match) { + const category = match[1].charAt(0).toUpperCase() + match[1].slice(1).toLowerCase(); + const content = summary.slice(match[0].length); + return { category, content }; + } + + // Default to "Changed" if no category prefix found + return { category: "Changed", content: summary }; +} + +/** + * Format a single release line for a changeset + */ +async function getReleaseLine(changeset, type, options) { + if (!options || !options.repo) { + throw new Error("Must provide options.repo for changelog-formatter"); + } + + const { summary } = changeset; + const { category, content } = parseChangeset(summary); + + let links = ""; + + try { + const info = await getInfo({ + repo: options.repo, + commit: changeset.commit, + }); + + // Format: [#PR](link) [`hash`](link) + const prLink = info.pull ? `[#${info.pull}](${info.links.pull})` : null; + const commitLink = info.commit ? `[\`${info.commit.slice(0, 7)}\`](${info.links.commit})` : null; + + const linkParts = [prLink, commitLink].filter(Boolean); + if (linkParts.length > 0) { + links = ` (${linkParts.join(" ")})`; + } + } catch (error) { + // If we can't get GitHub info, just continue without links + console.warn("Could not get GitHub info for changeset:", error.message); + } + + // Include category as HTML comment for post-processing + return `${content}${links}`; +} + +/** + * Format dependency update lines + */ +async function getDependencyReleaseLine(changesets, dependenciesUpdated, options) { + if (changesets.length === 0) return ""; + + const updatedDependencies = dependenciesUpdated.map( + (dependency) => ` - ${dependency.name}@${dependency.newVersion}` + ); + + return ["Updated dependencies:", ...updatedDependencies].join("\n"); +} + +module.exports = { + getReleaseLine, + getDependencyReleaseLine, +}; diff --git a/.changeset/config.json b/.changeset/config.json index 6944f60..5036abd 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", - "changelog": ["@changesets/changelog-github", { "repo": "plotday/plot" }], + "changelog": ["./.changeset/changelog-formatter.js", { "repo": "plotday/plot" }], "commit": false, "fixed": [], "linked": [], diff --git a/.changeset/fruity-horses-grow.md b/.changeset/fruity-horses-grow.md new file mode 100644 index 0000000..b1ee7dc --- /dev/null +++ b/.changeset/fruity-horses-grow.md @@ -0,0 +1,8 @@ +--- +"@plotday/sdk": patch +"@plotday/tool-google-calendar": patch +"@plotday/tool-google-contacts": patch +"@plotday/tool-outlook-calendar": patch +--- + +Changed: improved changelog format diff --git a/.changeset/reorganize-changelogs.js b/.changeset/reorganize-changelogs.js new file mode 100644 index 0000000..09836ff --- /dev/null +++ b/.changeset/reorganize-changelogs.js @@ -0,0 +1,160 @@ +#!/usr/bin/env node + +/** + * Post-processing script to reorganize CHANGELOG.md files + * Converts changesets' default grouping (Major/Minor/Patch Changes) + * into "Keep a Changelog" format (Added/Changed/Fixed/etc.) + */ + +const fs = require("fs"); +const path = require("path"); + +const CATEGORIES = [ + "Added", + "Changed", + "Deprecated", + "Removed", + "Fixed", + "Security" +]; + +/** + * Process a single CHANGELOG.md file + */ +function processChangelog(filePath) { + const content = fs.readFileSync(filePath, "utf-8"); + const lines = content.split("\n"); + + let result = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + + // Check if this is a version header (e.g., "## 1.0.0") + if (line.match(/^##\s+\d+\.\d+\.\d+/)) { + result.push(line); + i++; + + // Skip empty lines after version header + while (i < lines.length && lines[i].trim() === "") { + i++; + } + + // Check if we have change type sections (Minor Changes, Patch Changes, etc.) + const sectionStart = i; + const changes = []; + + // Collect all changes in this version + let currentCategory = null; + + while (i < lines.length && !lines[i].match(/^##\s+\d+\.\d+\.\d+/)) { + const currentLine = lines[i]; + + // Check for "Keep a Changelog" category headers (already formatted) + const keepAChangelogMatch = currentLine.match(/^###\s+(Added|Changed|Deprecated|Removed|Fixed|Security)$/); + if (keepAChangelogMatch) { + currentCategory = keepAChangelogMatch[1]; + i++; + continue; + } + + // Skip the old "### Minor Changes", "### Patch Changes" headers + if (currentLine.match(/^###\s+(Major|Minor|Patch)\s+Changes/)) { + currentCategory = null; // Reset category for old format + i++; + continue; + } + + // Check for category markers in the content (from changelog formatter) + const categoryMatch = currentLine.match(/(.*)/); + if (categoryMatch) { + const category = categoryMatch[1]; + const content = categoryMatch[2]; + changes.push({ category, content: `- ${content}` }); + i++; + } else if (currentLine.trim() !== "" && currentLine.match(/^-\s+/)) { + // If we have a current category (from Keep a Changelog format), use it + // Otherwise, treat as "Changed" (legacy/old format) + const category = currentCategory || "Changed"; + const contentLines = [currentLine]; + i++; + + // Collect any indented continuation lines (e.g., for dependency lists) + while (i < lines.length && lines[i].match(/^ /)) { + contentLines.push(lines[i]); + i++; + } + + changes.push({ category, content: contentLines.join("\n") }); + } else { + i++; + } + } + + // Group changes by category and output in "Keep a Changelog" order + if (changes.length > 0) { + result.push(""); // Empty line after version header + + for (const category of CATEGORIES) { + const categoryChanges = changes.filter(c => c.category === category); + if (categoryChanges.length > 0) { + result.push(`### ${category}`); + result.push(""); + categoryChanges.forEach(change => { + result.push(change.content); + }); + result.push(""); + } + } + } + } else { + // Keep non-version lines as-is (like the package name header) + result.push(line); + i++; + } + } + + // Write the reorganized content back + fs.writeFileSync(filePath, result.join("\n")); + console.log(`Reorganized: ${filePath}`); +} + +/** + * Recursively find CHANGELOG.md files + */ +function findChangelogFiles(dir, fileList = []) { + const files = fs.readdirSync(dir); + + files.forEach(file => { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + // Skip node_modules and hidden directories + if (file !== "node_modules" && !file.startsWith(".")) { + findChangelogFiles(filePath, fileList); + } + } else if (file === "CHANGELOG.md") { + fileList.push(filePath); + } + }); + + return fileList; +} + +/** + * Find and process all CHANGELOG.md files + */ +function main() { + const rootDir = path.join(__dirname, ".."); + const changelogFiles = findChangelogFiles(rootDir); + + console.log(`Found ${changelogFiles.length} changelog files`); + + changelogFiles.forEach(processChangelog); + + console.log("Done reorganizing changelogs!"); +} + +main(); diff --git a/.changeset/validate-changesets.js b/.changeset/validate-changesets.js new file mode 100644 index 0000000..2a548ff --- /dev/null +++ b/.changeset/validate-changesets.js @@ -0,0 +1,149 @@ +#!/usr/bin/env node + +/** + * Validates that all changeset files follow the required format + * with category prefixes: Added, Changed, Deprecated, Removed, Fixed, Security + */ + +const fs = require("fs"); +const path = require("path"); + +const CATEGORIES = [ + "Added", + "Changed", + "Deprecated", + "Removed", + "Fixed", + "Security" +]; + +const CATEGORY_PATTERN = new RegExp(`^(${CATEGORIES.join("|")}):\\s+`, "i"); + +/** + * Validate a single changeset file + */ +function validateChangeset(filePath) { + const content = fs.readFileSync(filePath, "utf-8"); + const lines = content.split("\n"); + + // Find the summary line (first non-empty line after the frontmatter) + let inFrontmatter = false; + let frontmatterClosed = false; + let summary = null; + + for (const line of lines) { + const trimmed = line.trim(); + + if (trimmed === "---") { + if (!inFrontmatter) { + inFrontmatter = true; + } else { + frontmatterClosed = true; + } + continue; + } + + if (frontmatterClosed && trimmed !== "") { + summary = trimmed; + break; + } + } + + if (!summary) { + return { + valid: false, + error: "No summary found in changeset" + }; + } + + // Check if summary has a valid category prefix + const match = summary.match(CATEGORY_PATTERN); + + if (!match) { + return { + valid: false, + error: `Summary must start with a category prefix (${CATEGORIES.join(", ")})`, + summary, + suggestion: `Example: "Added: ${summary}"` + }; + } + + // Check if the category is properly capitalized + const category = match[1]; + const properCategory = category.charAt(0).toUpperCase() + category.slice(1).toLowerCase(); + + if (category !== properCategory) { + return { + valid: false, + error: `Category "${category}" should be capitalized as "${properCategory}"`, + summary, + suggestion: summary.replace(match[0], `${properCategory}: `) + }; + } + + return { valid: true }; +} + +/** + * Get all changeset files + */ +function getChangesetFiles() { + const changesetDir = path.join(__dirname); + const files = fs.readdirSync(changesetDir); + + return files + .filter(file => file.endsWith(".md") && file !== "README.md") + .map(file => path.join(changesetDir, file)); +} + +/** + * Main validation + */ +function main() { + const changesetFiles = getChangesetFiles(); + + if (changesetFiles.length === 0) { + console.log("✓ No changesets to validate"); + process.exit(0); + } + + console.log(`Validating ${changesetFiles.length} changeset(s)...\n`); + + let hasErrors = false; + + for (const file of changesetFiles) { + const fileName = path.basename(file); + const result = validateChangeset(file); + + if (!result.valid) { + hasErrors = true; + console.error(`✗ ${fileName}`); + console.error(` ${result.error}`); + if (result.summary) { + console.error(` Current: "${result.summary}"`); + } + if (result.suggestion) { + console.error(` Fix: "${result.suggestion}"`); + } + console.error(""); + } else { + console.log(`✓ ${fileName}`); + } + } + + if (hasErrors) { + console.error("\nValidation failed!"); + console.error("\nChangeset summaries must start with one of these categories:"); + console.error(CATEGORIES.map(c => ` - ${c}: description`).join("\n")); + console.error("\nExamples:"); + console.error(" - Added: new authentication feature"); + console.error(" - Fixed: memory leak in worker process"); + console.error(" - Changed: improved error messages"); + process.exit(1); + } + + console.log("\n✓ All changesets are valid!"); + process.exit(0); +} + +main(); diff --git a/.github/workflows/changeset-check.yml b/.github/workflows/changeset-check.yml index 05ef513..15c013a 100644 --- a/.github/workflows/changeset-check.yml +++ b/.github/workflows/changeset-check.yml @@ -65,3 +65,6 @@ jobs: else echo "✅ Changeset found (count: $CHANGESET_COUNT)" fi + + - name: Validate changeset format + run: pnpm validate-changesets diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..ed13c03 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh + +# Validate changeset format before committing +pnpm validate-changesets diff --git a/package.json b/package.json index e2bd316..3cb7f02 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,15 @@ "lint": "pnpm -r run lint", "clean": "pnpm -r run clean", "changeset": "changeset", - "version-packages": "changeset version", - "release": "pnpm build && changeset publish" + "validate-changesets": "node .changeset/validate-changesets.js", + "version-packages": "changeset version && node .changeset/reorganize-changelogs.js", + "release": "pnpm build && changeset publish", + "prepare": "husky" }, "devDependencies": { "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.29.7", + "husky": "^9.1.7", "typescript": "^5.9.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 127a84a..eaed58e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@changesets/cli': specifier: ^2.29.7 version: 2.29.7(@types/node@24.7.2) + husky: + specifier: ^9.1.7 + version: 9.1.7 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -489,6 +492,11 @@ packages: resolution: {integrity: sha512-v/J+4Z/1eIJovEBdlV5TYj1IR+ZiohcYGRY+qN/oC9dAfKzVT023N/Bgw37hrKCoVRBvk3bqyzpr2PP5YeTMSg==} hasBin: true + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + iconv-lite@0.7.0: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} @@ -1150,6 +1158,8 @@ snapshots: human-id@4.1.2: {} + husky@9.1.7: {} + iconv-lite@0.7.0: dependencies: safer-buffer: 2.1.2 diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index ee6bc49..e2206f3 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -2,29 +2,28 @@ ## 0.11.0 -### Minor Changes +### Added -- [#7](https://github.com/plotday/plot/pull/7) [`1d809ec`](https://github.com/plotday/plot/commit/1d809ec778244921cda072eb3744f36e28b3c1b4) Thanks [@KrisBraun](https://github.com/KrisBraun)! - Add plot agent generate command +- plot agent generate command ([#7](https://github.com/plotday/plot/pull/7) [`1d809ec`](https://github.com/plotday/plot/commit/1d809ec778244921cda072eb3744f36e28b3c1b4)) ## 0.10.2 -### Patch Changes +### Added -- [#5](https://github.com/plotday/plot/pull/5) [`0ac9a95`](https://github.com/plotday/plot/commit/0ac9a953212ccd3abb3517e143e6a0957c061b14) Thanks [@KrisBraun](https://github.com/KrisBraun)! - Add a CLAUDE.md on "plot agent create" +- CLAUDE.md on "plot agent create" ([#5](https://github.com/plotday/plot/pull/5) [`0ac9a95`](https://github.com/plotday/plot/commit/0ac9a95953212ccd3abb3517e143e6a0957c061b14)) ## 0.10.1 -### Patch Changes +### Added -- [#3](https://github.com/plotday/plot/pull/3) [`61668e5`](https://github.com/plotday/plot/commit/61668e5fb6a640f0894f922bc852f2669dd4ea39) Thanks [@KrisBraun](https://github.com/KrisBraun)! - plot create --name argument - plot deploy --spec spec.md (alpha) +- plot create --name argument ([#3](https://github.com/plotday/plot/pull/3) [`61668e5`](https://github.com/plotday/plot/commit/61668e5fb6a640f0894f922bc852f2669dd4ea39)) ## 0.10.0 -### Minor Changes +### Added -- [#1](https://github.com/plotday/plot/pull/1) [`dce4f2f`](https://github.com/plotday/plot/commit/dce4f2ff3596bd9c73212c90a1cd49a7dac12f48) Thanks [@KrisBraun](https://github.com/KrisBraun)! - Add README.md and AGENTS.md on "plot agent create" +- README.md and AGENTS.md on "plot agent create" ([#1](https://github.com/plotday/plot/pull/1) [`dce4f2f`](https://github.com/plotday/plot/commit/dce4f2ff3596bd9c73212c90a1cd49a7dac12f48)) -### Patch Changes +### Changed -- [#1](https://github.com/plotday/plot/pull/1) [`a00de4c`](https://github.com/plotday/plot/commit/a00de4c48e3ec1d6190235d1d38fd3e5d398d480) Thanks [@KrisBraun](https://github.com/KrisBraun)! - Initial automated release setup +- Initial automated release setup ([#1](https://github.com/plotday/plot/pull/1) [`a00de4c`](https://github.com/plotday/plot/commit/a00de4c48e3ec1d6190235d1d38fd3e5d398d480)) diff --git a/tools/google-calendar/CHANGELOG.md b/tools/google-calendar/CHANGELOG.md index fa9c59e..a3f2460 100644 --- a/tools/google-calendar/CHANGELOG.md +++ b/tools/google-calendar/CHANGELOG.md @@ -2,16 +2,18 @@ ## 0.1.2 -### Patch Changes +### Changed - Updated dependencies [[`1d809ec`](https://github.com/plotday/plot/commit/1d809ec778244921cda072eb3744f36e28b3c1b4)]: - @plotday/sdk@0.11.0 ## 0.1.1 -### Patch Changes +### Added -- [#1](https://github.com/plotday/plot/pull/1) [`a00de4c`](https://github.com/plotday/plot/commit/a00de4c48e3ec1d6190235d1d38fd3e5d398d480) Thanks [@KrisBraun](https://github.com/KrisBraun)! - Initial automated release setup +- Initial automated release setup ([#1](https://github.com/plotday/plot/pull/1) [`a00de4c`](https://github.com/plotday/plot/commit/a00de4c48e3ec1d6190235d1d38fd3e5d398d480)) + +### Changed - Updated dependencies [[`a00de4c`](https://github.com/plotday/plot/commit/a00de4c48e3ec1d6190235d1d38fd3e5d398d480), [`dce4f2f`](https://github.com/plotday/plot/commit/dce4f2ff3596bd9c73212c90a1cd49a7dac12f48)]: - @plotday/sdk@0.10.0 diff --git a/tools/google-contacts/CHANGELOG.md b/tools/google-contacts/CHANGELOG.md index bfe2aa3..dd12a5c 100644 --- a/tools/google-contacts/CHANGELOG.md +++ b/tools/google-contacts/CHANGELOG.md @@ -2,16 +2,18 @@ ## 0.1.2 -### Patch Changes +### Changed - Updated dependencies [[`1d809ec`](https://github.com/plotday/plot/commit/1d809ec778244921cda072eb3744f36e28b3c1b4)]: - @plotday/sdk@0.11.0 ## 0.1.1 -### Patch Changes +### Added -- [#1](https://github.com/plotday/plot/pull/1) [`a00de4c`](https://github.com/plotday/plot/commit/a00de4c48e3ec1d6190235d1d38fd3e5d398d480) Thanks [@KrisBraun](https://github.com/KrisBraun)! - Initial automated release setup +- Initial automated release setup ([#1](https://github.com/plotday/plot/pull/1) [`a00de4c`](https://github.com/plotday/plot/commit/a00de4c48e3ec1d6190235d1d38fd3e5d398d480)) + +### Changed - Updated dependencies [[`a00de4c`](https://github.com/plotday/plot/commit/a00de4c48e3ec1d6190235d1d38fd3e5d398d480), [`dce4f2f`](https://github.com/plotday/plot/commit/dce4f2ff3596bd9c73212c90a1cd49a7dac12f48)]: - @plotday/sdk@0.10.0 diff --git a/tools/outlook-calendar/CHANGELOG.md b/tools/outlook-calendar/CHANGELOG.md index 8c8d03e..afafba9 100644 --- a/tools/outlook-calendar/CHANGELOG.md +++ b/tools/outlook-calendar/CHANGELOG.md @@ -2,16 +2,18 @@ ## 0.1.2 -### Patch Changes +### Changed - Updated dependencies [[`1d809ec`](https://github.com/plotday/plot/commit/1d809ec778244921cda072eb3744f36e28b3c1b4)]: - @plotday/sdk@0.11.0 ## 0.1.1 -### Patch Changes +### Added -- [#1](https://github.com/plotday/plot/pull/1) [`a00de4c`](https://github.com/plotday/plot/commit/a00de4c48e3ec1d6190235d1d38fd3e5d398d480) Thanks [@KrisBraun](https://github.com/KrisBraun)! - Initial automated release setup +- Initial automated release setup ([#1](https://github.com/plotday/plot/pull/1) [`a00de4c`](https://github.com/plotday/plot/commit/a00de4c48e3ec1d6190235d1d38fd3e5d398d480)) + +### Changed - Updated dependencies [[`a00de4c`](https://github.com/plotday/plot/commit/a00de4c48e3ec1d6190235d1d38fd3e5d398d480), [`dce4f2f`](https://github.com/plotday/plot/commit/dce4f2ff3596bd9c73212c90a1cd49a7dac12f48)]: - @plotday/sdk@0.10.0