Skip to content

Commit bc5cf9f

Browse files
committed
feat(release): implement medium-priority release automation improvements
Completes the remaining medium-priority items from the release agent review. Adds PR automation, Release Notes Manager agent, and comprehensive scope parameter documentation. ## Changes ### 1. PR Automation for release-prep.yml (✅ Complete) **Created: `scripts/create-release-pr.cjs`** - Automatically creates release PRs from develop to main - Computes next version based on merged PR labels - Updates VERSION and CHANGELOG.md files - Creates release branch (release/vX.Y.Z) - Generates comprehensive PR description with: - Changelog summary - Release checklist - Version bump information - Uses gh CLI for PR creation **Updated: `.github/workflows/release-prep.yml`** - Added "Create Release PR" step - Calls create-release-pr.cjs script - Passes GITHUB_TOKEN for authentication - Removes TODO comment (now implemented) **Features:** - Validates unreleased changes exist before creating PR - Detects if release branch already exists - Computes semantic version bumps (major/minor/patch) - Commits and pushes changes automatically - Comprehensive error handling and logging ### 2. Release Notes Manager Agent (✅ Complete) **Created: `.github/agents/release-notes-manager.agent.cjs`** Comprehensive release notes generation agent that: - Compiles clean release notes from changelog - Generates highlights from key sections (Added, Changed, Security) - Detects and flags breaking changes automatically - Lists contributors with PR counts - Groups changes by type with emojis (✨ Added, 🐛 Fixed, etc.) - Formats output suitable for GitHub releases - Includes installation instructions - Links to full changelog comparison **CLI Usage:** ```bash # Generate notes for specific version node .github/agents/release-notes-manager.agent.cjs --version=1.0.0 # Generate notes for latest version node .github/agents/release-notes-manager.agent.cjs --latest # Output to file node .github/agents/release-notes-manager.agent.cjs --version=1.0.0 --output=RELEASE_NOTES.md ``` **Features:** - ✅ Highlights (top 5 important changes) - ✅ Breaking changes detection and warnings - ✅ Grouped sections with emojis - ✅ Contributor attribution - ✅ Installation instructions - ✅ Full changelog links - ✅ Graceful error handling for missing tags **Implements spec:** `.github/agents/TODO/release-notes-manager.agents.md` ### 3. Scope Parameter Documentation (✅ Complete) **Created: `docs/RELEASE-SCOPE-GUIDE.md`** Comprehensive 400+ line guide covering: **Overview:** - What the --scope parameter does - Semantic versioning primer - How it affects version bumping **Detailed Sections:** - When to use each scope (patch/minor/major) - Real-world examples for each scope type - Decision flowchart for scope selection - Release label system integration **Examples:** - Bug fix release (patch) - New feature release (minor) - Breaking change release (major) - Dry-run testing examples **Best Practices:** - Validation before release - Documentation requirements - Testing procedures - Communication guidelines **FAQ:** - Common questions and answers - Edge cases and troubleshooting - Pre-release versions - Reverting releases - Monorepo considerations **Related Documentation:** - Links to release process guide - Agent specifications - External SemVer reference - Support channels ## Testing All components tested and working: ✅ **create-release-pr.cjs** - Script executes without errors - Validates unreleased changes - Handles missing tags gracefully ✅ **release-notes-manager.agent.cjs** - Generates comprehensive release notes for 0.1.0 - Handles missing git tags gracefully - Formats output with proper markdown - Includes highlights, breaking changes, grouped sections - Lists contributors (when available) ✅ **RELEASE-SCOPE-GUIDE.md** - Well-structured and comprehensive - Clear examples and use cases - Proper YAML frontmatter - Links to related documentation ## Benefits 1. **Automated PR Creation** - Reduces manual work for maintainers - Ensures consistent PR format - Validates changelog before PR creation - Integrates with existing workflows 2. **Professional Release Notes** - User-friendly format - Highlights key changes - Warns about breaking changes - Credits contributors 3. **Clear Documentation** - Removes ambiguity about version bumping - Provides decision framework - Real-world examples - Answers common questions ## Standards Compliance - ✅ UK English throughout - ✅ LightSpeed coding standards - ✅ Comprehensive JSDoc comments - ✅ YAML frontmatter in documentation - ✅ Error handling and logging - ✅ CommonJS compatibility (.cjs extension) ## Related Issues - Addresses medium-priority item #1: PR automation - Addresses medium-priority item #2: Release Notes Manager - Addresses medium-priority item #3: Scope documentation - Completes TODO in release-prep.yml (line 25) ## Next Steps The release automation is now feature-complete for MVP: - ✅ All critical issues resolved - ✅ All medium-priority items completed - ⏳ Low-priority polish items remain (optional) --- **Dependencies:** Requires .cjs scripts from previous commit **Breaking Changes:** None **Migration Required:** None
1 parent 2fe22c7 commit bc5cf9f

File tree

4 files changed

+1008
-1
lines changed

4 files changed

+1008
-1
lines changed
Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
#!/usr/bin/env node
2+
/**
3+
* ============================================================================
4+
* Agent: release-notes-manager.agent.cjs
5+
* Location: .github/agents/release-notes-manager.agent.cjs
6+
* Description:
7+
* - Compiles clean release notes from PRs, issues, and changelog
8+
* - Generates highlights and grouped sections
9+
* - Flags breaking changes and lists contributors
10+
* - Formats output suitable for GitHub releases
11+
* Standards:
12+
* - Follows LightSpeed Coding Standards
13+
* - See spec: .github/agents/TODO/release-notes-manager.agents.md
14+
* ============================================================================
15+
*/
16+
17+
const fs = require('fs');
18+
const path = require('path');
19+
const { execSync } = require('child_process');
20+
21+
// Import utilities
22+
const changelogUtilsPath = path.join(__dirname, 'includes/changelogUtils.cjs');
23+
const { parseChangelog, getLatestRelease } = require(changelogUtilsPath);
24+
25+
/**
26+
* Execute shell command
27+
*/
28+
function exec(cmd, options = {}) {
29+
try {
30+
return execSync(cmd, { encoding: 'utf8', ...options });
31+
} catch (error) {
32+
if (options.allowError) {
33+
return '';
34+
}
35+
throw new Error(`Command failed: ${cmd}\n${error.message}`);
36+
}
37+
}
38+
39+
/**
40+
* Get merged PRs for a version
41+
*/
42+
function getMergedPRs(fromTag, toTag = 'HEAD') {
43+
console.log(`Fetching PRs from ${fromTag || 'beginning'} to ${toTag}...`);
44+
45+
let gitLog = '';
46+
47+
try {
48+
if (fromTag) {
49+
// Check if tag exists
50+
exec(`git rev-parse ${fromTag}`, { allowError: false });
51+
gitLog = exec(`git log ${fromTag}..${toTag} --merges --format="%H|%s|%an|%ae"`, { allowError: true });
52+
} else {
53+
gitLog = exec(`git log ${toTag} --merges --format="%H|%s|%an|%ae"`, { allowError: true });
54+
}
55+
} catch (error) {
56+
console.warn(` Warning: Could not fetch git log (${error.message})`);
57+
console.warn(` Continuing with empty PR list...`);
58+
return [];
59+
}
60+
61+
if (!gitLog) {
62+
return [];
63+
}
64+
65+
const prPattern = /Merge pull request #(\d+) from (.+)/;
66+
const prs = [];
67+
68+
gitLog.split('\n').filter(Boolean).forEach(line => {
69+
const parts = line.split('|');
70+
if (parts.length >= 4) {
71+
const [hash, message, author, email] = parts;
72+
const match = message.match(prPattern);
73+
74+
if (match) {
75+
prs.push({
76+
number: match[1],
77+
branch: match[2],
78+
hash,
79+
message,
80+
author: { name: author, email }
81+
});
82+
}
83+
}
84+
});
85+
86+
return prs;
87+
}
88+
89+
/**
90+
* Get unique contributors
91+
*/
92+
function getContributors(prs) {
93+
const contributorsMap = new Map();
94+
95+
prs.forEach(pr => {
96+
const key = pr.author.email;
97+
if (!contributorsMap.has(key)) {
98+
contributorsMap.set(key, {
99+
name: pr.author.name,
100+
email: pr.author.email,
101+
prCount: 0
102+
});
103+
}
104+
contributorsMap.get(key).prCount += 1;
105+
});
106+
107+
return Array.from(contributorsMap.values()).sort((a, b) => b.prCount - a.prCount);
108+
}
109+
110+
/**
111+
* Detect breaking changes
112+
*/
113+
function detectBreakingChanges(changelogData, version) {
114+
const release = changelogData.releases.find(r => r.version === version);
115+
if (!release || !release.sections) {
116+
return [];
117+
}
118+
119+
const breakingChanges = [];
120+
121+
// Check for explicit breaking changes or major version bump indicators
122+
Object.keys(release.sections).forEach(section => {
123+
const items = release.sections[section] || [];
124+
items.forEach(item => {
125+
const lowerItem = item.toLowerCase();
126+
if (lowerItem.includes('breaking') ||
127+
lowerItem.includes('incompatible') ||
128+
lowerItem.includes('removed') ||
129+
(section === 'removed' && !lowerItem.includes('deprecated'))) {
130+
breakingChanges.push({ section, item });
131+
}
132+
});
133+
});
134+
135+
return breakingChanges;
136+
}
137+
138+
/**
139+
* Generate highlights from changelog
140+
*/
141+
function generateHighlights(changelogData, version) {
142+
const release = changelogData.releases.find(r => r.version === version);
143+
if (!release || !release.sections) {
144+
return [];
145+
}
146+
147+
const highlights = [];
148+
149+
// Priority sections for highlights
150+
const prioritySections = ['added', 'changed', 'security'];
151+
152+
prioritySections.forEach(section => {
153+
const items = release.sections[section] || [];
154+
// Take up to 3 items from high-priority sections
155+
items.slice(0, 3).forEach(item => {
156+
highlights.push({ section, item });
157+
});
158+
});
159+
160+
return highlights.slice(0, 5); // Max 5 highlights
161+
}
162+
163+
/**
164+
* Format release notes
165+
*/
166+
function formatReleaseNotes(options = {}) {
167+
const {
168+
version,
169+
changelogPath = 'CHANGELOG.md',
170+
includeContributors = true,
171+
includeBreakingChanges = true,
172+
includeHighlights = true
173+
} = options;
174+
175+
if (!version) {
176+
throw new Error('Version is required');
177+
}
178+
179+
console.log(`Generating release notes for version ${version}...`);
180+
181+
// Parse changelog
182+
const changelogData = parseChangelog(changelogPath);
183+
const release = changelogData.releases.find(r => r.version === version);
184+
185+
if (!release) {
186+
throw new Error(`Version ${version} not found in CHANGELOG`);
187+
}
188+
189+
// Get previous version tag
190+
const tagsOutput = exec('git tag --sort=-version:refname', { allowError: true });
191+
const tags = tagsOutput ? tagsOutput.split('\n').filter(Boolean) : [];
192+
const currentTag = `v${version}`;
193+
const currentTagIndex = tags.indexOf(currentTag);
194+
const previousTag = currentTagIndex >= 0 && currentTagIndex < tags.length - 1 ? tags[currentTagIndex + 1] : null;
195+
196+
// Get PRs and contributors
197+
const prs = getMergedPRs(previousTag, currentTag);
198+
const contributors = getContributors(prs);
199+
200+
// Detect breaking changes
201+
const breakingChanges = includeBreakingChanges ? detectBreakingChanges(changelogData, version) : [];
202+
203+
// Generate highlights
204+
const highlights = includeHighlights ? generateHighlights(changelogData, version) : [];
205+
206+
// Build release notes
207+
let notes = `# Release ${version}\n\n`;
208+
209+
// Highlights section
210+
if (highlights.length > 0) {
211+
notes += `## ✨ Highlights\n\n`;
212+
highlights.forEach(h => {
213+
notes += `- **${h.section.charAt(0).toUpperCase() + h.section.slice(1)}**: ${h.item}\n`;
214+
});
215+
notes += '\n';
216+
}
217+
218+
// Breaking changes warning
219+
if (breakingChanges.length > 0) {
220+
notes += `## ⚠️ Breaking Changes\n\n`;
221+
notes += `This release contains **${breakingChanges.length}** breaking change(s):\n\n`;
222+
breakingChanges.forEach(bc => {
223+
notes += `- ${bc.item}\n`;
224+
});
225+
notes += '\n';
226+
notes += `Please review the migration guide and update your code accordingly.\n\n`;
227+
}
228+
229+
// Grouped changes
230+
notes += `## 📋 Changes\n\n`;
231+
232+
const sectionOrder = ['added', 'changed', 'deprecated', 'removed', 'fixed', 'security', 'documentation', 'performance'];
233+
const sectionEmojis = {
234+
added: '✨',
235+
changed: '🔄',
236+
deprecated: '⚠️',
237+
removed: '🗑️',
238+
fixed: '🐛',
239+
security: '🔒',
240+
documentation: '📚',
241+
performance: '⚡'
242+
};
243+
244+
sectionOrder.forEach(section => {
245+
const items = release.sections[section] || [];
246+
if (items.length > 0) {
247+
const emoji = sectionEmojis[section] || '•';
248+
const title = section.charAt(0).toUpperCase() + section.slice(1);
249+
notes += `### ${emoji} ${title}\n\n`;
250+
items.forEach(item => {
251+
notes += `- ${item}\n`;
252+
});
253+
notes += '\n';
254+
}
255+
});
256+
257+
// Contributors section
258+
if (includeContributors && contributors.length > 0) {
259+
notes += `## 👥 Contributors\n\n`;
260+
notes += `This release was made possible by ${contributors.length} contributor(s):\n\n`;
261+
contributors.forEach(c => {
262+
notes += `- **${c.name}** (${c.prCount} PR${c.prCount > 1 ? 's' : ''})\n`;
263+
});
264+
notes += '\n';
265+
notes += `Thank you to everyone who contributed to this release! 🎉\n\n`;
266+
}
267+
268+
// Installation/upgrade section
269+
notes += `## 📦 Installation\n\n`;
270+
notes += `\`\`\`bash\n`;
271+
notes += `# Update to version ${version}\n`;
272+
notes += `npm install @lightspeedwp/github-community-health@${version}\n`;
273+
notes += `\`\`\`\n\n`;
274+
275+
// Metadata
276+
notes += `---\n\n`;
277+
notes += `**Full Changelog**: `;
278+
if (previousTag) {
279+
notes += `[\`${previousTag}...v${version}\`](../../compare/${previousTag}...v${version})\n`;
280+
} else {
281+
notes += `[View all changes](../../commits/v${version})\n`;
282+
}
283+
284+
return notes;
285+
}
286+
287+
/**
288+
* Main function
289+
*/
290+
async function run() {
291+
try {
292+
const args = process.argv.slice(2);
293+
294+
// Parse arguments
295+
let version = null;
296+
let outputFile = null;
297+
let format = 'markdown';
298+
299+
for (let i = 0; i < args.length; i++) {
300+
if (args[i].startsWith('--version=')) {
301+
version = args[i].split('=')[1];
302+
} else if (args[i].startsWith('--output=')) {
303+
outputFile = args[i].split('=')[1];
304+
} else if (args[i].startsWith('--format=')) {
305+
format = args[i].split('=')[1];
306+
} else if (args[i] === '--latest') {
307+
// Get latest version from changelog
308+
const changelogData = parseChangelog('CHANGELOG.md');
309+
const latest = getLatestRelease(changelogData);
310+
version = latest ? latest.version : null;
311+
} else if (!args[i].startsWith('--')) {
312+
version = args[i];
313+
}
314+
}
315+
316+
if (!version) {
317+
console.error('Usage: release-notes-manager.agent.cjs [--version=X.Y.Z | --latest] [--output=file.md] [--format=markdown]');
318+
console.error('');
319+
console.error('Examples:');
320+
console.error(' node release-notes-manager.agent.cjs --version=1.0.0');
321+
console.error(' node release-notes-manager.agent.cjs --latest');
322+
console.error(' node release-notes-manager.agent.cjs 1.0.0 --output=RELEASE_NOTES.md');
323+
process.exit(1);
324+
}
325+
326+
console.log('╔════════════════════════════════════════╗');
327+
console.log('║ Release Notes Manager Agent ║');
328+
console.log('╚════════════════════════════════════════╝\n');
329+
330+
const notes = formatReleaseNotes({ version });
331+
332+
if (outputFile) {
333+
fs.writeFileSync(outputFile, notes, 'utf8');
334+
console.log(`\n✅ Release notes written to: ${outputFile}`);
335+
} else {
336+
console.log('\n' + notes);
337+
}
338+
339+
} catch (error) {
340+
console.error('\n❌ Failed to generate release notes:', error.message);
341+
if (error.stack) {
342+
console.error(error.stack);
343+
}
344+
process.exit(1);
345+
}
346+
}
347+
348+
// Run if executed directly
349+
if (require.main === module) {
350+
run();
351+
}
352+
353+
module.exports = {
354+
formatReleaseNotes,
355+
getMergedPRs,
356+
getContributors,
357+
detectBreakingChanges,
358+
generateHighlights
359+
};

.github/workflows/release-prep.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ jobs:
2222
run: |
2323
node scripts/validate-version.cjs
2424
node scripts/validate-changelog.cjs
25-
# TODO: script to open PR with next version + CHANGELOG block
25+
- name: Create Release PR
26+
run: node scripts/create-release-pr.cjs
27+
env:
28+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2629
- name: Summary
2730
run: |
2831
echo "Will open PR with next version and schema-validated changelog" >> $GITHUB_STEP_SUMMARY

0 commit comments

Comments
 (0)