-
Notifications
You must be signed in to change notification settings - Fork 619
feat(changelog): fragment-based changelog system — no more CHANGELOG.md merge conflicts #754
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
c8e3bbf
4f6359f
95b8cd5
b7842b5
a59a814
94ddb12
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # RTK Changelog Fragment Schema | ||
| # One file per PR — never edit manually, use `pnpm changelog:add` | ||
|
|
||
| # Required fields | ||
| pr: 123 # PR number (must match filename prefix) | ||
| type: feat # feat | fix | perf | refactor | security | docs | chore | ||
| scope: "hook" # Functional scope (free text) | ||
| title: "Short title < 80 chars" # Will appear in CHANGELOG.md | ||
|
|
||
| # Optional fields | ||
| description: | | ||
| User-facing impact in 1-2 sentences (Markdown). | ||
| breaking: false # true → appears in Breaking Changes section | ||
| migration: false # true → shows ⚠️ Migration DB warning | ||
| scripts: [] # Post-deploy commands (array of strings) | ||
| # Example: | ||
| # scripts: | ||
| # - "node scripts/migrate-data.js --execute" | ||
|
|
||
| # Naming rule: {PR_NUMBER}-{slug-kebab-case}.yml | ||
| # The PR number in the filename MUST match the `pr` field. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| #!/usr/bin/env tsx | ||
| /** | ||
| * changelog:add — Interactive CLI to create a changelog fragment | ||
| * Usage: pnpm changelog:add | ||
| */ | ||
| import * as readline from "readline"; | ||
| import * as fs from "fs"; | ||
| import * as path from "path"; | ||
|
|
||
| const TYPES = ["feat", "fix", "perf", "refactor", "security", "docs", "chore"] as const; | ||
| type FragmentType = (typeof TYPES)[number]; | ||
|
|
||
| const FRAGMENTS_DIR = path.resolve(process.cwd(), "changelog/fragments"); | ||
|
|
||
| function slugify(text: string, maxLen = 40): string { | ||
| return text | ||
| .normalize("NFD") | ||
| .replace(/[\u0300-\u036f]/g, "") | ||
| .toLowerCase() | ||
| .replace(/[^a-z0-9]+/g, "-") | ||
| .replace(/^-+|-+$/g, "") | ||
| .slice(0, maxLen); | ||
| } | ||
|
|
||
| function ask(rl: readline.Interface, question: string): Promise<string> { | ||
| return new Promise((resolve) => rl.question(question, resolve)); | ||
| } | ||
|
|
||
| async function main() { | ||
| const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); | ||
|
|
||
| console.log("\n📝 Create a new changelog fragment\n"); | ||
|
|
||
| const prStr = await ask(rl, "PR number: "); | ||
| const pr = parseInt(prStr.trim(), 10); | ||
| if (isNaN(pr) || pr <= 0) { | ||
| console.error("❌ Invalid PR number"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log(`Types: ${TYPES.join(" | ")}`); | ||
| const typeInput = (await ask(rl, "Type: ")).trim() as FragmentType; | ||
| if (!TYPES.includes(typeInput)) { | ||
| console.error(`❌ Invalid type. Must be one of: ${TYPES.join(", ")}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const scope = (await ask(rl, "Scope (e.g. hook, git, permissions): ")).trim(); | ||
| if (!scope) { | ||
| console.error("❌ Scope is required"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const title = (await ask(rl, "Title (< 80 chars): ")).trim(); | ||
| if (!title) { | ||
| console.error("❌ Title is required"); | ||
| process.exit(1); | ||
| } | ||
| if (title.length > 80) { | ||
| console.error(`❌ Title too long: ${title.length} chars (max 80)`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| rl.close(); | ||
|
|
||
| const slug = slugify(title); | ||
| const filename = `${pr}-${slug}.yml`; | ||
| const filepath = path.join(FRAGMENTS_DIR, filename); | ||
|
|
||
| if (fs.existsSync(filepath)) { | ||
| console.error(`❌ File already exists: changelog/fragments/${filename}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const content = [ | ||
| `pr: ${pr}`, | ||
| `type: ${typeInput}`, | ||
| `scope: "${scope}"`, | ||
| `title: "${title}"`, | ||
| `description: |`, | ||
| ` TODO: describe the user-facing impact in 1-2 sentences.`, | ||
| `breaking: false`, | ||
| `migration: false`, | ||
| `scripts: []`, | ||
| ].join("\n") + "\n"; | ||
|
Comment on lines
+75
to
+85
|
||
|
|
||
| fs.mkdirSync(FRAGMENTS_DIR, { recursive: true }); | ||
| fs.writeFileSync(filepath, content, "utf8"); | ||
|
|
||
| console.log(`\n✅ Created: changelog/fragments/${filename}`); | ||
| console.log("\nNext steps:"); | ||
| console.log(` 1. Edit the description in changelog/fragments/${filename}`); | ||
| console.log(` 2. git add changelog/fragments/${filename}`); | ||
| console.log(` 3. git commit -s -m "docs(changelog): add fragment for PR #${pr}"`); | ||
| } | ||
|
|
||
| main().catch((e) => { | ||
| console.error(e); | ||
| process.exit(1); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
schema.ymlsays "never edit manually", but the documented workflow (and the generated fragment template) requires contributors to edit thedescriptionfield before committing. Adjust this wording to avoid telling contributors to avoid the exact manual edit they must do.