From 0e26ea6a68d2d61d40f7d33d7361a8fc58ed4d91 Mon Sep 17 00:00:00 2001 From: b3nw Date: Fri, 24 Oct 2025 04:24:00 -0500 Subject: [PATCH] fix: Add commit-based release notes to GitHub releases (#355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add commit-based release notes generation to GitHub releases. This PR updates the release workflow to generate release notes from git commits instead of extracting from CHANGELOG.md. The new system: - Automatically detects the previous tag for comparison - Categorizes commits using conventional commit types - Includes commit hashes and contributor statistics - Handles first release scenario gracefully Related: #362 (test architecture refactoring) Conceived by Romuald CzΕ‚onkowski - www.aiadvisors.pl/en --- .github/workflows/release.yml | 83 +++++++++++++------- scripts/generate-release-notes.js | 121 ++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 27 deletions(-) create mode 100644 scripts/generate-release-notes.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d4bf9c4..d3099b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -112,53 +112,82 @@ jobs: echo "βœ… Version $CURRENT_VERSION is valid (higher than npm version $NPM_VERSION)" - extract-changelog: - name: Extract Changelog + generate-release-notes: + name: Generate Release Notes runs-on: ubuntu-latest needs: detect-version-change if: needs.detect-version-change.outputs.version-changed == 'true' outputs: - release-notes: ${{ steps.extract.outputs.notes }} - has-notes: ${{ steps.extract.outputs.has-notes }} + release-notes: ${{ steps.generate.outputs.notes }} + has-notes: ${{ steps.generate.outputs.has-notes }} steps: - name: Checkout repository uses: actions/checkout@v4 - - - name: Extract changelog for version - id: extract + with: + fetch-depth: 0 # Need full history for git log + + - name: Generate release notes from commits + id: generate run: | - VERSION="${{ needs.detect-version-change.outputs.new-version }}" - CHANGELOG_FILE="docs/CHANGELOG.md" - - if [ ! -f "$CHANGELOG_FILE" ]; then - echo "Changelog file not found at $CHANGELOG_FILE" - echo "has-notes=false" >> $GITHUB_OUTPUT - echo "notes=No changelog entries found for version $VERSION" >> $GITHUB_OUTPUT - exit 0 - fi - - # Use the extracted changelog script - if NOTES=$(node scripts/extract-changelog.js "$VERSION" "$CHANGELOG_FILE" 2>/dev/null); then + CURRENT_VERSION="${{ needs.detect-version-change.outputs.new-version }}" + CURRENT_TAG="v$CURRENT_VERSION" + + # Get the previous tag (excluding the current tag which doesn't exist yet) + PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -v "^$CURRENT_TAG$" | head -1) + + echo "Current version: $CURRENT_VERSION" + echo "Current tag: $CURRENT_TAG" + echo "Previous tag: $PREVIOUS_TAG" + + if [ -z "$PREVIOUS_TAG" ]; then + echo "ℹ️ No previous tag found, this might be the first release" + + # Get all commits up to current commit + NOTES="### πŸŽ‰ Initial Release + +This is the initial release of n8n-mcp v$CURRENT_VERSION. + +--- + +**Release Statistics:** +- Commit count: $(git rev-list --count HEAD) +- First release setup" + echo "has-notes=true" >> $GITHUB_OUTPUT - + # Use heredoc to properly handle multiline content { echo "notes<> $GITHUB_OUTPUT - - echo "βœ… Successfully extracted changelog for version $VERSION" + else - echo "has-notes=false" >> $GITHUB_OUTPUT - echo "notes=No changelog entries found for version $VERSION" >> $GITHUB_OUTPUT - echo "⚠️ Could not extract changelog for version $VERSION" + echo "βœ… Previous tag found: $PREVIOUS_TAG" + + # Generate release notes between tags + if NOTES=$(node scripts/generate-release-notes.js "$PREVIOUS_TAG" "HEAD" 2>/dev/null); then + echo "has-notes=true" >> $GITHUB_OUTPUT + + # Use heredoc to properly handle multiline content + { + echo "notes<> $GITHUB_OUTPUT + + echo "βœ… Successfully generated release notes from $PREVIOUS_TAG to $CURRENT_TAG" + else + echo "has-notes=false" >> $GITHUB_OUTPUT + echo "notes=Failed to generate release notes for version $CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "⚠️ Could not generate release notes for version $CURRENT_VERSION" + fi fi create-release: name: Create GitHub Release runs-on: ubuntu-latest - needs: [detect-version-change, extract-changelog] + needs: [detect-version-change, generate-release-notes] if: needs.detect-version-change.outputs.version-changed == 'true' outputs: release-id: ${{ steps.create.outputs.id }} @@ -189,7 +218,7 @@ jobs: cat > release_body.md << 'EOF' # Release v${{ needs.detect-version-change.outputs.new-version }} - ${{ needs.extract-changelog.outputs.release-notes }} + ${{ needs.generate-release-notes.outputs.release-notes }} --- diff --git a/scripts/generate-release-notes.js b/scripts/generate-release-notes.js new file mode 100644 index 0000000..ee8d5bb --- /dev/null +++ b/scripts/generate-release-notes.js @@ -0,0 +1,121 @@ +#!/usr/bin/env node + +/** + * Generate release notes from commit messages between two tags + * Used by GitHub Actions to create automated release notes + */ + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +function generateReleaseNotes(previousTag, currentTag) { + try { + console.log(`Generating release notes from ${previousTag} to ${currentTag}`); + + // Get commits between tags + const gitLogCommand = `git log --pretty=format:"%H|%s|%an|%ae|%ad" --date=short --no-merges ${previousTag}..${currentTag}`; + const commitsOutput = execSync(gitLogCommand, { encoding: 'utf8' }); + + if (!commitsOutput.trim()) { + console.log('No commits found between tags'); + return 'No changes in this release.'; + } + + const commits = commitsOutput.trim().split('\n').map(line => { + const [hash, subject, author, email, date] = line.split('|'); + return { hash, subject, author, email, date }; + }); + + // Categorize commits + const categories = { + 'feat': { title: '✨ Features', commits: [] }, + 'fix': { title: 'πŸ› Bug Fixes', commits: [] }, + 'docs': { title: 'πŸ“š Documentation', commits: [] }, + 'refactor': { title: '♻️ Refactoring', commits: [] }, + 'test': { title: 'πŸ§ͺ Testing', commits: [] }, + 'perf': { title: '⚑ Performance', commits: [] }, + 'style': { title: 'πŸ’… Styling', commits: [] }, + 'ci': { title: 'πŸ”§ CI/CD', commits: [] }, + 'build': { title: 'πŸ“¦ Build', commits: [] }, + 'chore': { title: 'πŸ”§ Maintenance', commits: [] }, + 'other': { title: 'πŸ“ Other Changes', commits: [] } + }; + + commits.forEach(commit => { + const subject = commit.subject.toLowerCase(); + let categorized = false; + + // Check for conventional commit prefixes + for (const [prefix, category] of Object.entries(categories)) { + if (prefix !== 'other' && subject.startsWith(`${prefix}:`)) { + category.commits.push(commit); + categorized = true; + break; + } + } + + // If not categorized, put in other + if (!categorized) { + categories.other.commits.push(commit); + } + }); + + // Generate release notes + const releaseNotes = []; + + for (const [key, category] of Object.entries(categories)) { + if (category.commits.length > 0) { + releaseNotes.push(`### ${category.title}`); + releaseNotes.push(''); + + category.commits.forEach(commit => { + // Clean up the subject by removing the prefix if it exists + let cleanSubject = commit.subject; + const colonIndex = cleanSubject.indexOf(':'); + if (colonIndex !== -1 && cleanSubject.substring(0, colonIndex).match(/^(feat|fix|docs|refactor|test|perf|style|ci|build|chore)$/)) { + cleanSubject = cleanSubject.substring(colonIndex + 1).trim(); + // Capitalize first letter + cleanSubject = cleanSubject.charAt(0).toUpperCase() + cleanSubject.slice(1); + } + + releaseNotes.push(`- ${cleanSubject} (${commit.hash.substring(0, 7)})`); + }); + + releaseNotes.push(''); + } + } + + // Add commit statistics + const totalCommits = commits.length; + const contributors = [...new Set(commits.map(c => c.author))]; + + releaseNotes.push('---'); + releaseNotes.push(''); + releaseNotes.push(`**Release Statistics:**`); + releaseNotes.push(`- ${totalCommits} commit${totalCommits !== 1 ? 's' : ''}`); + releaseNotes.push(`- ${contributors.length} contributor${contributors.length !== 1 ? 's' : ''}`); + + if (contributors.length <= 5) { + releaseNotes.push(`- Contributors: ${contributors.join(', ')}`); + } + + return releaseNotes.join('\n'); + + } catch (error) { + console.error(`Error generating release notes: ${error.message}`); + return `Failed to generate release notes: ${error.message}`; + } +} + +// Parse command line arguments +const previousTag = process.argv[2]; +const currentTag = process.argv[3]; + +if (!previousTag || !currentTag) { + console.error('Usage: generate-release-notes.js '); + process.exit(1); +} + +const releaseNotes = generateReleaseNotes(previousTag, currentTag); +console.log(releaseNotes);