Folo/packages/changelog-cli/index.ts

225 lines
5.8 KiB
TypeScript

#!/usr/bin/env tsx
import "dotenv/config"
import { readFileSync, writeFileSync } from "node:fs"
import { fileURLToPath } from "node:url"
import { dirname, join } from "pathe"
import type { ChangelogEntry } from "./ai-agent"
import AIAgent from "./ai-agent"
import GitTools from "./git-tools"
// Get current directory
const __dirname = dirname(fileURLToPath(import.meta.url))
const changelogDir = join(__dirname, "..", "..", "apps", "mobile", "changelog")
const configPath = join(__dirname, "config.json")
interface Config {
internalTeamMembers: string[]
aiModel: {
provider: string
model: string
temperature: number
maxTokens: number
baseURL?: string
customEndpoint?: string
}
commitAnalysis: {
categories: Record<
string,
{
keywords: string[]
section: string
}
>
ignorePatterns: string[]
}
changelog: {
maxCommitsPerSection: number
includeCommitHash: boolean
includePullRequestLinks: boolean
}
}
/**
* Load configuration from file
*/
function loadConfig(): Config {
try {
const configContent = readFileSync(configPath, "utf-8")
return JSON.parse(configContent)
} catch (error) {
console.error("Failed to load config file:", error)
process.exit(1)
}
}
/**
* Format changelog entry for output
*/
function formatChangelogEntry(entry: ChangelogEntry, config: Config): string {
let line = `- ${entry.description}`
if (config.changelog.includePullRequestLinks && entry.pullRequest) {
line += `(#${entry.pullRequest})`
} else if (config.changelog.includeCommitHash && entry.commitHash) {
line += `(${entry.commitHash})`
}
return line
}
/**
* Group changelog entries by section
*/
function groupEntriesBySection(entries: ChangelogEntry[]): Record<string, ChangelogEntry[]> {
const grouped: Record<string, ChangelogEntry[]> = {
"Shiny new things": [],
Improvements: [],
"No longer broken": [],
}
entries.forEach((entry) => {
if (grouped[entry.section]) {
grouped[entry.section]!.push(entry)
}
})
return grouped
}
/**
* Generate the changelog content
*/
function generateChangelogContent(
version: string,
entries: ChangelogEntry[],
contributors: string[],
config: Config,
): string {
const groupedEntries = groupEntriesBySection(entries)
let content = `# What's New in v${version}\n\n`
// Add sections with entries
Object.entries(groupedEntries).forEach(([section, sectionEntries]) => {
content += `## ${section}\n\n`
if (sectionEntries.length > 0) {
// Limit entries per section
const limitedEntries = sectionEntries.slice(0, config.changelog.maxCommitsPerSection)
limitedEntries.forEach((entry) => {
content += `${formatChangelogEntry(entry, config)}\n`
})
}
content += "\n"
})
// Add contributors section
content += "## Thanks\n\n"
if (contributors.length > 0) {
const contributorList = contributors.map((name) => `@${name}`).join(" ")
content += `Special thanks to volunteer contributors ${contributorList} for their valuable contributions\n`
} else {
content += "Special thanks to volunteer contributors @ for their valuable contributions\n"
}
return content
}
/**
* Main function to generate changelog
*/
async function main() {
try {
console.info("🤖 Starting AI-driven changelog generation...")
// Load configuration
const config = loadConfig()
console.info("✅ Configuration loaded")
// Initialize Git tools
const git = new GitTools()
// Check if we're on a release branch
if (!git.isOnReleaseBranch()) {
console.error("❌ This script should only be run on a release branch (release/mobile/*)")
process.exit(1)
}
// Get version from branch name
const version = git.getVersionFromBranch()
if (!version) {
console.error("❌ Could not extract version from branch name")
process.exit(1)
}
console.info(`📝 Generating changelog for version: ${version}`)
// Get the latest mobile tag
const latestTag = git.getLatestMobileTag()
if (!latestTag) {
console.error("❌ No previous mobile tags found")
process.exit(1)
}
console.info(`📌 Latest tag: ${latestTag.name}`)
// Get commits between latest tag and current HEAD
const currentCommit = git.getLatestCommitHash()
const commits = git.getCommitsBetween(latestTag.hash, currentCommit)
console.info(`🔍 Found ${commits.length} commits since last release`)
if (commits.length === 0) {
console.warn("⚠️ No new commits found since last release")
return
}
// Initialize AI agent
const aiAgent = new AIAgent(config.aiModel, config.commitAnalysis.categories as any)
// Analyze commits with AI
const changelogEntries = await aiAgent.analyzeCommits(commits)
console.info(`✨ Generated ${changelogEntries.length} changelog entries`)
// Get contributors
const contributors = git.getUniqueAuthors(commits, config.internalTeamMembers)
console.info(`👥 Found ${contributors.length} external contributors`)
// Generate changelog content
const changelogContent = generateChangelogContent(
version,
changelogEntries,
contributors,
config,
)
// Write to next.md
const nextFilePath = join(changelogDir, "next.md")
writeFileSync(nextFilePath, changelogContent, "utf-8")
console.info("✅ Changelog generated successfully!")
console.info(`📄 Updated: ${nextFilePath}`)
// Show preview
console.info("\n📋 Preview:")
console.info("─".repeat(50))
console.info(changelogContent)
console.info("─".repeat(50))
} catch (error) {
console.error("❌ Failed to generate changelog:", error)
process.exit(1)
}
}
// Run the script if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
main()
}
export default main