@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.
- package/LICENSE +21 -0
- package/README.md +1493 -0
- package/__tests__/analyzer.test.js +63 -0
- package/__tests__/categorizer.test.js +93 -0
- package/__tests__/config.test.js +92 -0
- package/__tests__/formatter.test.js +63 -0
- package/__tests__/formatters.test.js +394 -0
- package/__tests__/migration.test.js +322 -0
- package/__tests__/semver.test.js +94 -0
- package/__tests__/tones.test.js +252 -0
- package/action.yml +113 -0
- package/index.js +73 -0
- package/jest.config.js +10 -0
- package/package.json +41 -0
- package/src/ai-writer.js +108 -0
- package/src/analyzer.js +232 -0
- package/src/analyzers/migration.js +355 -0
- package/src/categorizer.js +182 -0
- package/src/config.js +162 -0
- package/src/constants.js +137 -0
- package/src/contributor.js +144 -0
- package/src/diff-analyzer.js +202 -0
- package/src/formatter.js +336 -0
- package/src/formatters/discord.js +174 -0
- package/src/formatters/html.js +195 -0
- package/src/formatters/index.js +42 -0
- package/src/formatters/markdown.js +123 -0
- package/src/formatters/slack.js +176 -0
- package/src/formatters/twitter.js +242 -0
- package/src/formatters/types.js +48 -0
- package/src/generator.js +297 -0
- package/src/integrations/changelog.js +125 -0
- package/src/integrations/discord.js +96 -0
- package/src/integrations/github-release.js +75 -0
- package/src/integrations/indexer.js +119 -0
- package/src/integrations/slack.js +112 -0
- package/src/integrations/twitter.js +128 -0
- package/src/logger.js +52 -0
- package/src/prompts.js +210 -0
- package/src/rate-limiter.js +92 -0
- package/src/semver.js +129 -0
- package/src/tones/casual.js +114 -0
- package/src/tones/humorous.js +164 -0
- package/src/tones/index.js +38 -0
- package/src/tones/professional.js +125 -0
- package/src/tones/technical.js +164 -0
- 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
|
+
};
|