mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
fix: Add commit-based release notes to GitHub releases (#355)
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
This commit is contained in:
71
.github/workflows/release.yml
vendored
71
.github/workflows/release.yml
vendored
@@ -112,33 +112,47 @@ jobs:
|
|||||||
|
|
||||||
echo "✅ Version $CURRENT_VERSION is valid (higher than npm version $NPM_VERSION)"
|
echo "✅ Version $CURRENT_VERSION is valid (higher than npm version $NPM_VERSION)"
|
||||||
|
|
||||||
extract-changelog:
|
generate-release-notes:
|
||||||
name: Extract Changelog
|
name: Generate Release Notes
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: detect-version-change
|
needs: detect-version-change
|
||||||
if: needs.detect-version-change.outputs.version-changed == 'true'
|
if: needs.detect-version-change.outputs.version-changed == 'true'
|
||||||
outputs:
|
outputs:
|
||||||
release-notes: ${{ steps.extract.outputs.notes }}
|
release-notes: ${{ steps.generate.outputs.notes }}
|
||||||
has-notes: ${{ steps.extract.outputs.has-notes }}
|
has-notes: ${{ steps.generate.outputs.has-notes }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Need full history for git log
|
||||||
|
|
||||||
- name: Extract changelog for version
|
- name: Generate release notes from commits
|
||||||
id: extract
|
id: generate
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ needs.detect-version-change.outputs.new-version }}"
|
CURRENT_VERSION="${{ needs.detect-version-change.outputs.new-version }}"
|
||||||
CHANGELOG_FILE="docs/CHANGELOG.md"
|
CURRENT_TAG="v$CURRENT_VERSION"
|
||||||
|
|
||||||
if [ ! -f "$CHANGELOG_FILE" ]; then
|
# Get the previous tag (excluding the current tag which doesn't exist yet)
|
||||||
echo "Changelog file not found at $CHANGELOG_FILE"
|
PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -v "^$CURRENT_TAG$" | head -1)
|
||||||
echo "has-notes=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "notes=No changelog entries found for version $VERSION" >> $GITHUB_OUTPUT
|
echo "Current version: $CURRENT_VERSION"
|
||||||
exit 0
|
echo "Current tag: $CURRENT_TAG"
|
||||||
fi
|
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"
|
||||||
|
|
||||||
# Use the extracted changelog script
|
|
||||||
if NOTES=$(node scripts/extract-changelog.js "$VERSION" "$CHANGELOG_FILE" 2>/dev/null); then
|
|
||||||
echo "has-notes=true" >> $GITHUB_OUTPUT
|
echo "has-notes=true" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Use heredoc to properly handle multiline content
|
# Use heredoc to properly handle multiline content
|
||||||
@@ -148,17 +162,32 @@ jobs:
|
|||||||
echo "EOF"
|
echo "EOF"
|
||||||
} >> $GITHUB_OUTPUT
|
} >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
echo "✅ Successfully extracted changelog for version $VERSION"
|
else
|
||||||
|
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<<EOF"
|
||||||
|
echo "$NOTES"
|
||||||
|
echo "EOF"
|
||||||
|
} >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
echo "✅ Successfully generated release notes from $PREVIOUS_TAG to $CURRENT_TAG"
|
||||||
else
|
else
|
||||||
echo "has-notes=false" >> $GITHUB_OUTPUT
|
echo "has-notes=false" >> $GITHUB_OUTPUT
|
||||||
echo "notes=No changelog entries found for version $VERSION" >> $GITHUB_OUTPUT
|
echo "notes=Failed to generate release notes for version $CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "⚠️ Could not extract changelog for version $VERSION"
|
echo "⚠️ Could not generate release notes for version $CURRENT_VERSION"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
create-release:
|
create-release:
|
||||||
name: Create GitHub Release
|
name: Create GitHub Release
|
||||||
runs-on: ubuntu-latest
|
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'
|
if: needs.detect-version-change.outputs.version-changed == 'true'
|
||||||
outputs:
|
outputs:
|
||||||
release-id: ${{ steps.create.outputs.id }}
|
release-id: ${{ steps.create.outputs.id }}
|
||||||
@@ -189,7 +218,7 @@ jobs:
|
|||||||
cat > release_body.md << 'EOF'
|
cat > release_body.md << 'EOF'
|
||||||
# Release v${{ needs.detect-version-change.outputs.new-version }}
|
# Release v${{ needs.detect-version-change.outputs.new-version }}
|
||||||
|
|
||||||
${{ needs.extract-changelog.outputs.release-notes }}
|
${{ needs.generate-release-notes.outputs.release-notes }}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
121
scripts/generate-release-notes.js
Normal file
121
scripts/generate-release-notes.js
Normal file
@@ -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 <previous-tag> <current-tag>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseNotes = generateReleaseNotes(previousTag, currentTag);
|
||||||
|
console.log(releaseNotes);
|
||||||
Reference in New Issue
Block a user