@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,137 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ DEFAULT_CATEGORIES: [
5
+ { label: '🚀 Features', patterns: ['feat', 'feature', 'add', 'new'] },
6
+ { label: '🐛 Bug Fixes', patterns: ['fix', 'bugfix', 'patch', 'hotfix'] },
7
+ { label: '💥 Breaking Changes', patterns: ['breaking', 'BREAKING CHANGE', 'BREAKING'] },
8
+ { label: '⚡ Performance', patterns: ['perf', 'performance', 'optimize', 'speed'] },
9
+ { label: '♻️ Refactoring', patterns: ['refactor', 'restructure', 'reorganize'] },
10
+ { label: '📝 Documentation', patterns: ['docs', 'doc', 'readme', 'documentation'] },
11
+ { label: '🎨 Style', patterns: ['style', 'format', 'lint', 'prettier'] },
12
+ { label: '🧪 Tests', patterns: ['test', 'spec', 'coverage'] },
13
+ { label: '🔧 Chore', patterns: ['chore', 'build', 'ci', 'deps', 'dependabot'] },
14
+ { label: '🔒 Security', patterns: ['security', 'vulnerability', 'CVE'] },
15
+ ],
16
+
17
+ LANGUAGES: {
18
+ en: {
19
+ title: 'Release Notes',
20
+ summary: 'Summary',
21
+ contributors: 'Contributors',
22
+ breaking: 'Breaking Changes',
23
+ fullChangelog: 'Full Changelog',
24
+ features: 'Features',
25
+ bugFixes: 'Bug Fixes',
26
+ other: 'Other Changes',
27
+ },
28
+ es: {
29
+ title: 'Notas de la Versión',
30
+ summary: 'Resumen',
31
+ contributors: 'Colaboradores',
32
+ breaking: 'Cambios Importantes',
33
+ fullChangelog: 'Registro Completo',
34
+ features: 'Características',
35
+ bugFixes: 'Correcciones',
36
+ other: 'Otros Cambios',
37
+ },
38
+ fr: {
39
+ title: 'Notes de Version',
40
+ summary: 'Résumé',
41
+ contributors: 'Contributeurs',
42
+ breaking: 'Changements Cassants',
43
+ fullChangelog: 'Journal Complet',
44
+ features: 'Fonctionnalités',
45
+ bugFixes: 'Corrections',
46
+ other: 'Autres Changements',
47
+ },
48
+ de: {
49
+ title: 'Versionshinweise',
50
+ summary: 'Zusammenfassung',
51
+ contributors: 'Mitwirkende',
52
+ breaking: 'Breaking Changes',
53
+ fullChangelog: 'Vollständiges Änderungsprotokoll',
54
+ features: 'Funktionen',
55
+ bugFixes: 'Fehlerbehebungen',
56
+ other: 'Sonstige Änderungen',
57
+ },
58
+ ja: {
59
+ title: 'リリースノート',
60
+ summary: '概要',
61
+ contributors: '貢献者',
62
+ breaking: '重大な変更',
63
+ fullChangelog: '完全な変更履歴',
64
+ features: '機能',
65
+ bugFixes: 'バグ修正',
66
+ other: 'その他の変更',
67
+ },
68
+ ko: {
69
+ title: '릴리스 노트',
70
+ summary: '요약',
71
+ contributors: '기여자',
72
+ breaking: '주요 변경사항',
73
+ fullChangelog: '전체 변경 이력',
74
+ features: '기능',
75
+ bugFixes: '버그 수정',
76
+ other: '기타 변경사항',
77
+ },
78
+ zh: {
79
+ title: '发布说明',
80
+ summary: '摘要',
81
+ contributors: '贡献者',
82
+ breaking: '破坏性变更',
83
+ fullChangelog: '完整变更日志',
84
+ features: '新功能',
85
+ bugFixes: '错误修复',
86
+ other: '其他变更',
87
+ },
88
+ pt: {
89
+ title: 'Notas de Versão',
90
+ summary: 'Resumo',
91
+ contributors: 'Contribuidores',
92
+ breaking: 'Mudanças Importantes',
93
+ fullChangelog: 'Registro Completo',
94
+ features: 'Funcionalidades',
95
+ bugFixes: 'Correções',
96
+ other: 'Outras Mudanças',
97
+ },
98
+ ru: {
99
+ title: 'Примечания к релизу',
100
+ summary: 'Обзор',
101
+ contributors: 'Участники',
102
+ breaking: 'Критические изменения',
103
+ fullChangelog: 'Полный журнал изменений',
104
+ features: 'Функции',
105
+ bugFixes: 'Исправления',
106
+ other: 'Прочие изменения',
107
+ },
108
+ },
109
+
110
+ DEFAULTS: {
111
+ TEMPLATE: 'default',
112
+ COMMIT_MODE: 'auto',
113
+ VERSION_FROM: 'tag',
114
+ MAX_COMMITS: 200,
115
+ LANGUAGE: 'en',
116
+ TEMPERATURE: 0.3,
117
+ MAX_TOKENS: 4000,
118
+ },
119
+
120
+ CONVENTIONAL_COMMIT_REGEX: /^(feat|fix|perf|refactor|docs|style|test|chore|build|ci|revert)(\(([^)]+)\))?(!)?:\s*(.+)/i,
121
+
122
+ BREAKING_CHANGE_REGEX: /BREAKING[ -]CHANGE:\s*(.+)/i,
123
+
124
+ BUILT_IN_TEMPLATES: ['default', 'minimal', 'detailed', 'enterprise', 'fun'],
125
+
126
+ VALID_COMMIT_MODES: ['commits', 'pull-requests', 'auto'],
127
+
128
+ VALID_VERSION_SOURCES: ['tag', 'package-json', 'manual'],
129
+
130
+ VALID_LANGUAGES: ['en', 'es', 'fr', 'de', 'ja', 'ko', 'zh', 'pt', 'ru'],
131
+
132
+ IMPACT_THRESHOLDS: {
133
+ LOW: 5,
134
+ MEDIUM: 20,
135
+ HIGH: Infinity,
136
+ },
137
+ };
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ const logger = require('./logger');
4
+
5
+ /**
6
+ * Get contributors from commits and PRs.
7
+ * Detects first-time contributors, counts contributions, sorts by count.
8
+ */
9
+ function getContributors(commits, prs) {
10
+ const contributorMap = new Map();
11
+
12
+ // Collect from commits
13
+ if (commits && Array.isArray(commits)) {
14
+ for (const commit of commits) {
15
+ try {
16
+ const author = commit.author || (commit.commit && commit.commit.author) || {};
17
+ const login =
18
+ author.login ||
19
+ (author.name) ||
20
+ '';
21
+ const avatarUrl = author.avatar_url || '';
22
+ const name = author.name || author.login || login;
23
+
24
+ if (!login) continue;
25
+
26
+ if (contributorMap.has(login)) {
27
+ const existing = contributorMap.get(login);
28
+ existing.commits_count += 1;
29
+ } else {
30
+ contributorMap.set(login, {
31
+ login: login,
32
+ name: name,
33
+ avatar_url: avatarUrl,
34
+ commits_count: 1,
35
+ is_first_time: false,
36
+ });
37
+ }
38
+ } catch (err) {
39
+ logger.debug(`Failed to process commit author: ${err.message}`);
40
+ }
41
+ }
42
+ }
43
+
44
+ // Collect from PRs
45
+ if (prs && Array.isArray(prs)) {
46
+ for (const pr of prs) {
47
+ try {
48
+ const user = pr.user || {};
49
+ const login = user.login || '';
50
+ if (!login) continue;
51
+
52
+ if (contributorMap.has(login)) {
53
+ contributorMap.get(login).commits_count += 1;
54
+ } else {
55
+ contributorMap.set(login, {
56
+ login: login,
57
+ name: user.name || login,
58
+ avatar_url: user.avatar_url || '',
59
+ commits_count: 1,
60
+ is_first_time: false,
61
+ });
62
+ }
63
+
64
+ // Add reviewers
65
+ if (pr.requested_reviewers && Array.isArray(pr.requested_reviewers)) {
66
+ for (const reviewer of pr.requested_reviewers) {
67
+ const rLogin = reviewer.login || '';
68
+ if (!rLogin) continue;
69
+ if (!contributorMap.has(rLogin)) {
70
+ contributorMap.set(rLogin, {
71
+ login: rLogin,
72
+ name: reviewer.name || rLogin,
73
+ avatar_url: reviewer.avatar_url || '',
74
+ commits_count: 0,
75
+ is_first_time: false,
76
+ });
77
+ }
78
+ }
79
+ }
80
+ } catch (err) {
81
+ logger.debug(`Failed to process PR contributor: ${err.message}`);
82
+ }
83
+ }
84
+ }
85
+
86
+ // Sort by contribution count (descending)
87
+ const contributors = Array.from(contributorMap.values()).sort(
88
+ (a, b) => b.commits_count - a.commits_count
89
+ );
90
+
91
+ return contributors;
92
+ }
93
+
94
+ /**
95
+ * Detect first-time contributors by comparing against known contributors from previous releases.
96
+ */
97
+ function detectFirstTimers(contributors, previousContributors) {
98
+ const previousSet = new Set((previousContributors || []).map(c => c.login || c));
99
+
100
+ return contributors.map(c => ({
101
+ ...c,
102
+ is_first_time: !previousSet.has(c.login),
103
+ }));
104
+ }
105
+
106
+ /**
107
+ * Format a contributor thanks string.
108
+ * Example: "Thanks to @user1, @user2, @user3 and @user4 for their contributions!"
109
+ */
110
+ function formatContributorThanks(contributors) {
111
+ if (!contributors || contributors.length === 0) {
112
+ return '';
113
+ }
114
+
115
+ const firstTimers = contributors.filter(c => c.is_first_time);
116
+ const allLogins = contributors.map(c => `@${c.login}`);
117
+
118
+ let thanks = '';
119
+ if (allLogins.length === 1) {
120
+ thanks = `Thanks to ${allLogins[0]} for this release!`;
121
+ } else if (allLogins.length === 2) {
122
+ thanks = `Thanks to ${allLogins[0]} and ${allLogins[1]} for their contributions!`;
123
+ } else if (allLogins.length <= 5) {
124
+ const last = allLogins.pop();
125
+ thanks = `Thanks to ${allLogins.join(', ')} and ${last} for their contributions!`;
126
+ } else {
127
+ const displayed = allLogins.slice(0, 5);
128
+ const remaining = allLogins.length - 5;
129
+ thanks = `Thanks to ${displayed.join(', ')} and ${remaining} other contributor${remaining > 1 ? 's' : ''} for their contributions!`;
130
+ }
131
+
132
+ if (firstTimers.length > 0) {
133
+ const firstTimerLogins = firstTimers.map(c => `@${c.login}`);
134
+ thanks += `\n\n🎉 First-time contributors: ${firstTimerLogins.join(', ')}`;
135
+ }
136
+
137
+ return thanks;
138
+ }
139
+
140
+ module.exports = {
141
+ getContributors,
142
+ detectFirstTimers,
143
+ formatContributorThanks,
144
+ };
@@ -0,0 +1,202 @@
1
+ 'use strict';
2
+
3
+ const { IMPACT_THRESHOLDS } = require('./constants');
4
+ const logger = require('./logger');
5
+
6
+ /**
7
+ * Analyze diffs for a set of commits via the GitHub API.
8
+ * @param {Array} commits - Commits to analyze
9
+ * @param {Object} octokit - Octokit instance
10
+ * @param {Object} context - GitHub Actions context
11
+ * @returns {Object} Diff summary with file stats, impact, affected areas
12
+ */
13
+ async function analyzeDiff(commits, octokit, context) {
14
+ const result = {
15
+ files_changed: 0,
16
+ files_added: 0,
17
+ files_modified: 0,
18
+ files_deleted: 0,
19
+ additions: 0,
20
+ deletions: 0,
21
+ impact: 'low',
22
+ affected_areas: [],
23
+ potential_breaking: [],
24
+ files: [],
25
+ };
26
+
27
+ if (!commits || commits.length === 0 || !octokit || !context) {
28
+ logger.debug('Skipping diff analysis: no commits or no octokit/context');
29
+ return result;
30
+ }
31
+
32
+ const owner = context.repo.owner;
33
+ const repo = context.repo.repo;
34
+
35
+ for (const commit of commits) {
36
+ try {
37
+ const sha = commit.sha || commit.id;
38
+ if (!sha) continue;
39
+
40
+ const { data: commitData } = await octokit.rest.repos.getCommit({
41
+ owner,
42
+ repo,
43
+ ref: sha,
44
+ });
45
+
46
+ const files = commitData.files || [];
47
+ for (const file of files) {
48
+ result.files.push({
49
+ filename: file.filename,
50
+ status: file.status,
51
+ additions: file.additions,
52
+ deletions: file.deletions,
53
+ changes: file.changes,
54
+ });
55
+
56
+ result.additions += file.additions || 0;
57
+ result.deletions += file.deletions || 0;
58
+ result.files_changed += 1;
59
+
60
+ if (file.status === 'added') result.files_added += 1;
61
+ else if (file.status === 'modified') result.files_modified += 1;
62
+ else if (file.status === 'removed') result.files_deleted += 1;
63
+
64
+ // Detect affected areas
65
+ detectAffectedAreas(file.filename, result.affected_areas);
66
+
67
+ // Detect potential breaking changes
68
+ detectPotentialBreaking(file, result.potential_breaking);
69
+ }
70
+ } catch (err) {
71
+ logger.debug(`Failed to get diff for commit ${commit.sha}: ${err.message}`);
72
+ }
73
+ }
74
+
75
+ // Remove duplicate affected areas
76
+ result.affected_areas = [...new Set(result.affected_areas)];
77
+
78
+ // Determine impact level
79
+ if (result.files_changed <= IMPACT_THRESHOLDS.LOW) {
80
+ result.impact = 'low';
81
+ } else if (result.files_changed <= IMPACT_THRESHOLDS.MEDIUM) {
82
+ result.impact = 'medium';
83
+ } else {
84
+ result.impact = 'high';
85
+ }
86
+
87
+ logger.info(`Diff analysis: ${result.files_changed} files changed, impact=${result.impact}, areas=${result.affected_areas.join(',')}`);
88
+
89
+ return result;
90
+ }
91
+
92
+ /**
93
+ * Detect which areas of the codebase are affected by a file change.
94
+ */
95
+ function detectAffectedAreas(filename, areas) {
96
+ if (!filename) return;
97
+
98
+ const lower = filename.toLowerCase();
99
+
100
+ if (lower.includes('route') || lower.includes('controller') || lower.includes('api/') || lower.includes('endpoint')) {
101
+ areas.push('API');
102
+ }
103
+ if (lower.includes('model') || lower.includes('schema') || lower.includes('migration') || lower.includes('db/') || lower.includes('database')) {
104
+ areas.push('Database');
105
+ }
106
+ if (lower.includes('auth') || lower.includes('login') || lower.includes('token') || lower.includes('session') || lower.includes('password')) {
107
+ areas.push('Authentication');
108
+ }
109
+ if (lower.includes('component') || lower.includes('page') || lower.includes('view') || lower.includes('ui/') || lower.includes('style')) {
110
+ areas.push('UI');
111
+ }
112
+ if (lower.includes('config') || lower.includes('.env') || lower.includes('setting')) {
113
+ areas.push('Configuration');
114
+ }
115
+ if (lower.includes('test') || lower.includes('spec') || lower.includes('__test__')) {
116
+ areas.push('Tests');
117
+ }
118
+ if (lower.includes('middleware') || lower.includes('interceptor') || lower.includes('filter')) {
119
+ areas.push('Middleware');
120
+ }
121
+ if (lower.includes('util') || lower.includes('helper') || lower.includes('service')) {
122
+ areas.push('Utilities');
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Detect potential breaking changes from a diff.
128
+ */
129
+ function detectPotentialBreaking(file, potentialBreaking) {
130
+ if (!file) return;
131
+
132
+ const filename = (file.filename || '').toLowerCase();
133
+
134
+ // Removed exports in index files
135
+ if (file.status === 'removed' && (filename.includes('index.') || filename.includes('export'))) {
136
+ potentialBreaking.push({
137
+ file: file.filename,
138
+ reason: 'Export file was removed',
139
+ });
140
+ }
141
+
142
+ // Large deletion ratio might indicate removed functionality
143
+ if (file.deletions > file.additions * 3 && file.deletions > 20) {
144
+ potentialBreaking.push({
145
+ file: file.filename,
146
+ reason: `Significant code removal (${file.deletions} deletions vs ${file.additions} additions)`,
147
+ });
148
+ }
149
+
150
+ // Changes to public API files
151
+ if (filename.includes('api') && filename.includes('public')) {
152
+ if (file.status === 'modified') {
153
+ potentialBreaking.push({
154
+ file: file.filename,
155
+ reason: 'Public API file was modified',
156
+ });
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Format diff stats for display in release notes.
163
+ */
164
+ function formatDiffStats(diffSummary) {
165
+ if (!diffSummary || diffSummary.files_changed === 0) {
166
+ return '';
167
+ }
168
+
169
+ const lines = [];
170
+ lines.push(`- **${diffSummary.files_changed}** file${diffSummary.files_changed !== 1 ? 's' : ''} changed`);
171
+ if (diffSummary.files_added > 0) {
172
+ lines.push(`- **${diffSummary.files_added}** file${diffSummary.files_added !== 1 ? 's' : ''} added`);
173
+ }
174
+ if (diffSummary.files_modified > 0) {
175
+ lines.push(`- **${diffSummary.files_modified}** file${diffSummary.files_modified !== 1 ? 's' : ''} modified`);
176
+ }
177
+ if (diffSummary.files_deleted > 0) {
178
+ lines.push(`- **${diffSummary.files_deleted}** file${diffSummary.files_deleted !== 1 ? 's' : ''} deleted`);
179
+ }
180
+ lines.push(`- **+${diffSummary.additions}** additions, **-${diffSummary.deletions}** deletions`);
181
+
182
+ if (diffSummary.impact === 'high') {
183
+ lines.push(`- Impact: **High** (20+ files changed)`);
184
+ } else if (diffSummary.impact === 'medium') {
185
+ lines.push(`- Impact: **Medium** (6-20 files changed)`);
186
+ } else {
187
+ lines.push(`- Impact: **Low** (1-5 files changed)`);
188
+ }
189
+
190
+ if (diffSummary.affected_areas.length > 0) {
191
+ lines.push(`- Affected areas: ${diffSummary.affected_areas.join(', ')}`);
192
+ }
193
+
194
+ return lines.join('\n');
195
+ }
196
+
197
+ module.exports = {
198
+ analyzeDiff,
199
+ detectAffectedAreas,
200
+ detectPotentialBreaking,
201
+ formatDiffStats,
202
+ };