@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
package/src/constants.js
ADDED
|
@@ -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
|
+
};
|