@theihtisham/ai-release-notes 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1493 -0
  3. package/__tests__/analyzer.test.js +63 -0
  4. package/__tests__/categorizer.test.js +93 -0
  5. package/__tests__/config.test.js +92 -0
  6. package/__tests__/formatter.test.js +63 -0
  7. package/__tests__/formatters.test.js +394 -0
  8. package/__tests__/migration.test.js +322 -0
  9. package/__tests__/semver.test.js +94 -0
  10. package/__tests__/tones.test.js +252 -0
  11. package/action.yml +113 -0
  12. package/index.js +73 -0
  13. package/jest.config.js +10 -0
  14. package/package.json +41 -0
  15. package/src/ai-writer.js +108 -0
  16. package/src/analyzer.js +232 -0
  17. package/src/analyzers/migration.js +355 -0
  18. package/src/categorizer.js +182 -0
  19. package/src/config.js +162 -0
  20. package/src/constants.js +137 -0
  21. package/src/contributor.js +144 -0
  22. package/src/diff-analyzer.js +202 -0
  23. package/src/formatter.js +336 -0
  24. package/src/formatters/discord.js +174 -0
  25. package/src/formatters/html.js +195 -0
  26. package/src/formatters/index.js +42 -0
  27. package/src/formatters/markdown.js +123 -0
  28. package/src/formatters/slack.js +176 -0
  29. package/src/formatters/twitter.js +242 -0
  30. package/src/formatters/types.js +48 -0
  31. package/src/generator.js +297 -0
  32. package/src/integrations/changelog.js +125 -0
  33. package/src/integrations/discord.js +96 -0
  34. package/src/integrations/github-release.js +75 -0
  35. package/src/integrations/indexer.js +119 -0
  36. package/src/integrations/slack.js +112 -0
  37. package/src/integrations/twitter.js +128 -0
  38. package/src/logger.js +52 -0
  39. package/src/prompts.js +210 -0
  40. package/src/rate-limiter.js +92 -0
  41. package/src/semver.js +129 -0
  42. package/src/tones/casual.js +114 -0
  43. package/src/tones/humorous.js +164 -0
  44. package/src/tones/index.js +38 -0
  45. package/src/tones/professional.js +125 -0
  46. package/src/tones/technical.js +164 -0
  47. package/src/tones/types.js +26 -0
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ const logger = require('../logger');
4
+ const { createRelease } = require('./github-release');
5
+ const { postToSlack } = require('./slack');
6
+ const { postToDiscord } = require('./discord');
7
+ const { postTweet } = require('./twitter');
8
+ const { updateChangelog } = require('./changelog');
9
+
10
+ /**
11
+ * Route release data to all configured integrations.
12
+ * Executes in parallel where possible.
13
+ * Never fails the action — logs errors and continues.
14
+ *
15
+ * @param {Object} config - Configuration object
16
+ * @param {Object} releaseData - { version, notes, summary, url, contributors }
17
+ * @param {Object} octokit - Octokit instance (optional)
18
+ * @param {Object} context - GitHub Actions context (optional)
19
+ * @returns {Object} Results from each integration
20
+ */
21
+ async function postToIntegrations(config, releaseData, octokit, context) {
22
+ const results = {};
23
+
24
+ logger.info('Posting to configured integrations...');
25
+
26
+ // GitHub Release (always attempt if octokit/context available)
27
+ if (octokit && context && !config.dryRun) {
28
+ try {
29
+ const releaseResult = await createRelease(
30
+ octokit,
31
+ context,
32
+ releaseData.version,
33
+ releaseData.notes,
34
+ config
35
+ );
36
+ results.githubRelease = {
37
+ success: true,
38
+ url: releaseResult.url,
39
+ created: releaseResult.created,
40
+ };
41
+ releaseData.url = releaseResult.url;
42
+ } catch (err) {
43
+ results.githubRelease = { success: false, message: err.message };
44
+ logger.error('GitHub Release integration failed', err);
45
+ }
46
+ }
47
+
48
+ // Run remaining integrations in parallel
49
+ const parallelTasks = [];
50
+
51
+ // Slack
52
+ if (config.hasSlack && !config.dryRun) {
53
+ parallelTasks.push(
54
+ postToSlack(config.slackWebhook, releaseData)
55
+ .then(result => { results.slack = result; })
56
+ .catch(err => {
57
+ results.slack = { success: false, message: err.message };
58
+ logger.error('Slack integration failed', err);
59
+ })
60
+ );
61
+ }
62
+
63
+ // Discord
64
+ if (config.hasDiscord && !config.dryRun) {
65
+ parallelTasks.push(
66
+ postToDiscord(config.discordWebhook, releaseData)
67
+ .then(result => { results.discord = result; })
68
+ .catch(err => {
69
+ results.discord = { success: false, message: err.message };
70
+ logger.error('Discord integration failed', err);
71
+ })
72
+ );
73
+ }
74
+
75
+ // Twitter
76
+ if (config.hasTwitter && !config.dryRun) {
77
+ parallelTasks.push(
78
+ postTweet(
79
+ {
80
+ consumerKey: config.twitterConsumerKey,
81
+ consumerSecret: config.twitterConsumerSecret,
82
+ accessToken: config.twitterAccessToken,
83
+ accessSecret: config.twitterAccessSecret,
84
+ },
85
+ releaseData
86
+ )
87
+ .then(result => { results.twitter = result; })
88
+ .catch(err => {
89
+ results.twitter = { success: false, message: err.message };
90
+ logger.error('Twitter integration failed', err);
91
+ })
92
+ );
93
+ }
94
+
95
+ // Changelog
96
+ if (config.updateChangelog && octokit && context && !config.dryRun) {
97
+ parallelTasks.push(
98
+ updateChangelog(octokit, context, config, releaseData.notes, releaseData.version)
99
+ .then(result => { results.changelog = result; })
100
+ .catch(err => {
101
+ results.changelog = { success: false, message: err.message };
102
+ logger.error('Changelog integration failed', err);
103
+ })
104
+ );
105
+ }
106
+
107
+ // Wait for all parallel tasks to complete
108
+ await Promise.all(parallelTasks);
109
+
110
+ // Log summary
111
+ const summary = Object.entries(results)
112
+ .map(([name, result]) => `${name}: ${result.success ? 'OK' : 'FAILED'}`)
113
+ .join(', ');
114
+ logger.info(`Integration results: ${summary || 'none configured'}`);
115
+
116
+ return results;
117
+ }
118
+
119
+ module.exports = { postToIntegrations };
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ const logger = require('../logger');
4
+
5
+ /**
6
+ * Post release notes to Slack via webhook.
7
+ * @param {string} webhookUrl - Slack webhook URL
8
+ * @param {Object} releaseData - { version, notes, summary, url, contributors }
9
+ * @returns {Object} { success, message }
10
+ */
11
+ async function postToSlack(webhookUrl, releaseData) {
12
+ try {
13
+ if (!webhookUrl) {
14
+ logger.debug('No Slack webhook provided — skipping');
15
+ return { success: false, message: 'No webhook URL' };
16
+ }
17
+
18
+ const { version, summary, url, contributors } = releaseData;
19
+ const topChanges = extractTopChanges(releaseData.notes, 5);
20
+
21
+ const blocks = [
22
+ {
23
+ type: 'header',
24
+ text: {
25
+ type: 'plain_text',
26
+ text: `New Release: v${version}`,
27
+ emoji: true,
28
+ },
29
+ },
30
+ {
31
+ type: 'section',
32
+ text: {
33
+ type: 'mrkdwn',
34
+ text: summary || `Version ${version} has been released!`,
35
+ },
36
+ },
37
+ ];
38
+
39
+ if (topChanges.length > 0) {
40
+ blocks.push({
41
+ type: 'section',
42
+ text: {
43
+ type: 'mrkdwn',
44
+ text: `*Key Changes:*\n${topChanges.map(c => ` - ${c}`).join('\n')}`,
45
+ },
46
+ });
47
+ }
48
+
49
+ if (url) {
50
+ blocks.push({
51
+ type: 'actions',
52
+ elements: [
53
+ {
54
+ type: 'button',
55
+ text: {
56
+ type: 'plain_text',
57
+ text: 'View Full Release Notes',
58
+ },
59
+ url: url,
60
+ },
61
+ ],
62
+ });
63
+ }
64
+
65
+ if (contributors && contributors.length > 0) {
66
+ const names = contributors.slice(0, 5).map(c => `@${c.login}`).join(', ');
67
+ blocks.push({
68
+ type: 'context',
69
+ elements: [
70
+ {
71
+ type: 'mrkdwn',
72
+ text: `Contributors: ${names}${contributors.length > 5 ? ` and ${contributors.length - 5} more` : ''}`,
73
+ },
74
+ ],
75
+ });
76
+ }
77
+
78
+ const response = await fetch(webhookUrl, {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({ blocks }),
82
+ });
83
+
84
+ if (!response.ok) {
85
+ const text = await response.text();
86
+ throw new Error(`Slack API returned ${response.status}: ${text}`);
87
+ }
88
+
89
+ logger.info('Slack notification sent successfully');
90
+ return { success: true, message: 'Posted to Slack' };
91
+ } catch (err) {
92
+ logger.error('Failed to post to Slack', err);
93
+ return { success: false, message: err.message };
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Extract top N changes from release notes markdown.
99
+ */
100
+ function extractTopChanges(notes, count) {
101
+ if (!notes) return [];
102
+
103
+ const lines = notes.split('\n');
104
+ const bullets = lines
105
+ .filter(line => line.match(/^-\s+/))
106
+ .map(line => line.replace(/^-\s+/, '').trim())
107
+ .filter(line => line.length > 0 && line.length < 150);
108
+
109
+ return bullets.slice(0, count);
110
+ }
111
+
112
+ module.exports = { postToSlack };
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ const logger = require('../logger');
4
+
5
+ /**
6
+ * Post a release tweet via Twitter API v2.
7
+ * @param {Object} credentials - { consumerKey, consumerSecret, accessToken, accessSecret }
8
+ * @param {Object} releaseData - { version, summary, url, notes }
9
+ * @returns {Object} { success, message, tweetUrl }
10
+ */
11
+ async function postTweet(credentials, releaseData) {
12
+ try {
13
+ if (!credentials || !credentials.consumerKey) {
14
+ logger.debug('No Twitter credentials provided — skipping');
15
+ return { success: false, message: 'No credentials' };
16
+ }
17
+
18
+ const { version, summary, url } = releaseData;
19
+
20
+ // Generate tweet text
21
+ const tweetText = generateTweetText(version, summary, releaseData.notes, url);
22
+
23
+ logger.info(`Posting tweet (${tweetText.length} chars): ${tweetText.substring(0, 50)}...`);
24
+
25
+ // Use twitter-api-v2 with OAuth 1.0a
26
+ const { TwitterApi } = require('twitter-api-v2');
27
+
28
+ const client = new TwitterApi({
29
+ appKey: credentials.consumerKey,
30
+ appSecret: credentials.consumerSecret,
31
+ accessToken: credentials.accessToken,
32
+ accessSecret: credentials.accessSecret,
33
+ });
34
+
35
+ const tweetClient = client.readWrite;
36
+
37
+ const result = await tweetClient.v2.tweet(tweetText);
38
+
39
+ const tweetId = result.data.id;
40
+ const tweetUrl = `https://twitter.com/i/status/${tweetId}`;
41
+
42
+ logger.info(`Tweet posted: ${tweetUrl}`);
43
+ return { success: true, message: 'Tweet posted', tweetUrl };
44
+ } catch (err) {
45
+ logger.error('Failed to post tweet', err);
46
+ return { success: false, message: err.message };
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Generate tweet text within 280 character limit.
52
+ */
53
+ function generateTweetText(version, summary, notes, url) {
54
+ const topFeature = extractTopFeature(notes);
55
+ const topFix = extractTopFix(notes);
56
+
57
+ let text = `v${version} is here!`;
58
+
59
+ if (summary && summary.length < 100) {
60
+ text = `v${version} is here! ${summary}`;
61
+ }
62
+
63
+ // Add top feature and fix
64
+ const extras = [];
65
+ if (topFeature) extras.push(`Feature: ${topFeature}`);
66
+ if (topFix) extras.push(`Fix: ${topFix}`);
67
+
68
+ if (extras.length > 0) {
69
+ const extraText = extras.join(' | ');
70
+ const withoutUrl = `${text}\n\n${extraText}\n\n#changelog #release`;
71
+ if (url && withoutUrl.length + url.length + 2 <= 280) {
72
+ return `${withoutUrl}\n${url}`;
73
+ }
74
+ if (withoutUrl.length <= 280) {
75
+ return withoutUrl;
76
+ }
77
+ }
78
+
79
+ // Simplified: just version, short summary, and link
80
+ const base = `v${version} released! `;
81
+ const hashtags = '#changelog #release';
82
+
83
+ if (url) {
84
+ const available = 280 - base.length - url.length - hashtags.length - 6;
85
+ const shortSummary = summary ? summary.substring(0, Math.max(0, available)) : '';
86
+ return `${base}${shortSummary}\n${url}\n\n${hashtags}`;
87
+ }
88
+
89
+ return `${base}${hashtags}`;
90
+ }
91
+
92
+ /**
93
+ * Extract top feature description from notes.
94
+ */
95
+ function extractTopFeature(notes) {
96
+ if (!notes) return null;
97
+
98
+ const lines = notes.split('\n');
99
+ for (const line of lines) {
100
+ const cleaned = line.replace(/^[-*]\s+/, '').trim();
101
+ if (cleaned.length > 10 && cleaned.length < 80) {
102
+ return cleaned;
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+
108
+ /**
109
+ * Extract top fix description from notes.
110
+ */
111
+ function extractTopFix(notes) {
112
+ if (!notes) return null;
113
+
114
+ const lines = notes.split('\n');
115
+ const fixes = lines.filter(
116
+ l => l.toLowerCase().includes('fix') || l.toLowerCase().includes('bug')
117
+ );
118
+
119
+ for (const line of fixes) {
120
+ const cleaned = line.replace(/^[-*]\s+/, '').trim();
121
+ if (cleaned.length > 10 && cleaned.length < 80) {
122
+ return cleaned;
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+
128
+ module.exports = { postTweet, generateTweetText };
package/src/logger.js ADDED
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const core = require('@actions/core');
4
+
5
+ class Logger {
6
+ constructor(debugEnabled = false) {
7
+ this.debugEnabled = debugEnabled;
8
+ }
9
+
10
+ debug(message, data) {
11
+ if (this.debugEnabled) {
12
+ core.debug(message);
13
+ if (data) {
14
+ core.debug(JSON.stringify(data, null, 2));
15
+ }
16
+ }
17
+ }
18
+
19
+ info(message) {
20
+ core.info(message);
21
+ }
22
+
23
+ warn(message) {
24
+ core.warning(message);
25
+ }
26
+
27
+ error(message, err) {
28
+ if (err) {
29
+ core.error(`${message}: ${err.message || err}`);
30
+ if (err.stack && this.debugEnabled) {
31
+ core.debug(err.stack);
32
+ }
33
+ } else {
34
+ core.error(message);
35
+ }
36
+ }
37
+
38
+ group(name) {
39
+ core.startGroup(name);
40
+ }
41
+
42
+ groupEnd() {
43
+ core.endGroup();
44
+ }
45
+
46
+ setDebugEnabled(enabled) {
47
+ this.debugEnabled = enabled;
48
+ }
49
+ }
50
+
51
+ module.exports = new Logger();
52
+ module.exports.Logger = Logger;
package/src/prompts.js ADDED
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ const { LANGUAGES } = require('./constants');
4
+
5
+ /**
6
+ * Get the system prompt for AI release note generation.
7
+ * @param {Object} config - Configuration object
8
+ * @returns {string} System prompt
9
+ */
10
+ function getSystemPrompt(config) {
11
+ const lang = LANGUAGES[config.language] || LANGUAGES.en;
12
+ const styleGuidance = getStyleGuidance(config.template);
13
+
14
+ return `You are an expert technical writer specializing in software release notes. Your task is to generate clear, informative, and well-structured release notes from commit and PR data.
15
+
16
+ ## Language
17
+ Write the release notes in ${config.language === 'en' ? 'English' : lang.title} (${config.language}).
18
+
19
+ ## Style
20
+ ${styleGuidance}
21
+
22
+ ## Rules
23
+ 1. Use past tense ("Added", "Fixed", "Improved" — not "Add", "Fix", "Improve")
24
+ 2. Group related changes logically under category headings
25
+ 3. Include PR numbers as clickable markdown links: [#123](https://github.com/example/repo/pull/123) — use the repo from context
26
+ 4. For breaking changes, always include a "Migration Guide" subsection with specific steps
27
+ 5. Be specific and concrete ("Added user avatar upload API endpoint at POST /api/users/avatar" not "Added new features")
28
+ 6. Maximum 2000 words
29
+ 7. Use proper markdown formatting with headers (##), bullet points (-), bold (**), and code blocks where appropriate
30
+ 8. Start with a one-paragraph summary of the release
31
+ 9. Do not include empty sections
32
+ 10. Include contributor acknowledgments at the end
33
+ 11. Preserve any technical details, version numbers, or configuration references from the source data
34
+ 12. Handle unicode characters correctly in commit messages
35
+
36
+ ## Output Format
37
+ Return ONLY the markdown content for the release notes body. Do not include a top-level heading (h1) — start with a summary paragraph or h2 sections.`;
38
+ }
39
+
40
+ /**
41
+ * Get style guidance based on template.
42
+ */
43
+ function getStyleGuidance(template) {
44
+ switch (template) {
45
+ case 'enterprise':
46
+ return `Write in a formal, professional tone suitable for enterprise audiences. Include:
47
+ - Executive Summary section (2-3 sentences)
48
+ - Structured Change sections with clear categorization
49
+ - Impact Assessment for each significant change
50
+ - Action Items section (what users should do after upgrading)
51
+ - Known Issues section (if applicable)
52
+ - Avoid emojis. Use clear, precise language.`;
53
+
54
+ case 'fun':
55
+ return `Write in a casual, celebratory tone! Use emojis liberally. Include:
56
+ - Enthusiastic language ("This release is packed with awesomeness!")
57
+ - Fun metaphors and analogies
58
+ - Celebrate contributor achievements
59
+ - Use emojis for section headers and bullet points
60
+ - Keep it light but still informative
61
+ - End with something encouraging`;
62
+
63
+ case 'minimal':
64
+ return `Be extremely concise. Use simple bullet points only.
65
+ - One line per change
66
+ - No sections, no summary paragraph
67
+ - Just the facts, ma'am
68
+ - PR number and brief description`;
69
+
70
+ case 'detailed':
71
+ return `Be thorough and comprehensive. Include:
72
+ - Detailed summary paragraph
73
+ - Every change with full context
74
+ - Author credits for each change
75
+ - Diff statistics and affected areas
76
+ - Linked issues with full URLs
77
+ - Migration guides for breaking changes
78
+ - Screenshots section (if images detected)`;
79
+
80
+ case 'default':
81
+ default:
82
+ return `Use a balanced, developer-friendly tone. Be clear and concise but friendly. Include:
83
+ - Brief summary paragraph
84
+ - Categorized changes with bullet points
85
+ - Contributor thanks section
86
+ - Full changelog link`;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Get the user prompt with structured data from the analysis.
92
+ * @param {Object} analysis - Analysis results from analyzer
93
+ * @param {Object} diffSummary - Diff analysis results
94
+ * @param {string} version - Current version
95
+ * @param {Object} config - Configuration object
96
+ * @returns {string} User prompt
97
+ */
98
+ function getUserPrompt(analysis, diffSummary, version, config) {
99
+ const sections = [];
100
+
101
+ sections.push(`# Release Data for v${version}`);
102
+ sections.push('');
103
+ sections.push(`## Repository: ${config.repoFullName || 'unknown'}`);
104
+ sections.push(`## Date: ${new Date().toISOString().split('T')[0]}`);
105
+ sections.push('');
106
+
107
+ // Stats
108
+ if (analysis.stats) {
109
+ sections.push('## Statistics');
110
+ sections.push(`- Total commits: ${analysis.stats.total_commits}`);
111
+ sections.push(`- Total PRs: ${analysis.stats.total_prs}`);
112
+ sections.push(`- Files changed: ${analysis.stats.total_files_changed}`);
113
+ sections.push(`- Additions: ${analysis.stats.additions}`);
114
+ sections.push(`- Deletions: ${analysis.stats.deletions}`);
115
+ sections.push('');
116
+ }
117
+
118
+ // Categories
119
+ if (analysis.categories) {
120
+ sections.push('## Categorized Changes');
121
+ for (const [category, changes] of Object.entries(analysis.categories)) {
122
+ sections.push(`### ${category}`);
123
+ for (const change of changes) {
124
+ let line = `- ${change.description}`;
125
+ if (change.pr) {
126
+ line += ` (#${change.pr})`;
127
+ }
128
+ if (change.scope) {
129
+ line += ` [${change.scope}]`;
130
+ }
131
+ if (change.author) {
132
+ line += ` by @${change.author}`;
133
+ }
134
+ sections.push(line);
135
+ }
136
+ sections.push('');
137
+ }
138
+ }
139
+
140
+ // Breaking changes
141
+ if (analysis.breaking && analysis.breaking.length > 0) {
142
+ sections.push('## Breaking Changes');
143
+ for (const bc of analysis.breaking) {
144
+ sections.push(`- ${bc.description}`);
145
+ if (bc.migration_guide) {
146
+ sections.push(` Migration: ${bc.migration_guide}`);
147
+ }
148
+ if (bc.scope) {
149
+ sections.push(` Scope: ${bc.scope}`);
150
+ }
151
+ }
152
+ sections.push('');
153
+ }
154
+
155
+ // Contributors
156
+ if (analysis.contributors && analysis.contributors.length > 0) {
157
+ sections.push('## Contributors');
158
+ for (const c of analysis.contributors) {
159
+ sections.push(`- @${c.login} (${c.commits_count} commit${c.commits_count !== 1 ? 's' : ''})${c.is_first_time ? ' [FIRST TIME]' : ''}`);
160
+ }
161
+ sections.push('');
162
+ }
163
+
164
+ // Diff summary
165
+ if (diffSummary && diffSummary.files_changed > 0) {
166
+ sections.push('## Diff Summary');
167
+ sections.push(`- Files changed: ${diffSummary.files_changed} (${diffSummary.files_added} added, ${diffSummary.files_modified} modified, ${diffSummary.files_deleted} deleted)`);
168
+ sections.push(`- Lines: +${diffSummary.additions} / -${diffSummary.deletions}`);
169
+ sections.push(`- Impact: ${diffSummary.impact}`);
170
+ if (diffSummary.affected_areas.length > 0) {
171
+ sections.push(`- Affected areas: ${diffSummary.affected_areas.join(', ')}`);
172
+ }
173
+ if (diffSummary.potential_breaking.length > 0) {
174
+ sections.push('- Potential breaking changes detected:');
175
+ for (const pb of diffSummary.potential_breaking) {
176
+ sections.push(` - ${pb.file}: ${pb.reason}`);
177
+ }
178
+ }
179
+ sections.push('');
180
+ }
181
+
182
+ // Scopes
183
+ if (analysis.scopes && Object.keys(analysis.scopes).length > 0) {
184
+ sections.push('## Scopes');
185
+ for (const [scope, count] of Object.entries(analysis.scopes)) {
186
+ sections.push(`- ${scope}: ${count} change${count !== 1 ? 's' : ''}`);
187
+ }
188
+ sections.push('');
189
+ }
190
+
191
+ // Linked issues
192
+ if (analysis.linkedIssues && analysis.linkedIssues.length > 0) {
193
+ sections.push('## Linked Issues');
194
+ for (const issue of analysis.linkedIssues) {
195
+ sections.push(`- #${issue.number}`);
196
+ }
197
+ sections.push('');
198
+ }
199
+
200
+ sections.push('---');
201
+ sections.push('Generate the release notes now based on the above data.');
202
+
203
+ return sections.join('\n');
204
+ }
205
+
206
+ module.exports = {
207
+ getSystemPrompt,
208
+ getUserPrompt,
209
+ getStyleGuidance,
210
+ };