@theihtisham/ai-release-notes 1.0.0 → 1.1.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 (55) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +43 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +33 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +18 -0
  5. package/.github/dependabot.yml +16 -0
  6. package/.github/workflows/ci.yml +24 -0
  7. package/CODE_OF_CONDUCT.md +27 -0
  8. package/LICENSE +21 -21
  9. package/README.md +126 -1493
  10. package/SECURITY.md +22 -0
  11. package/__tests__/analyzer.test.js +63 -63
  12. package/__tests__/categorizer.test.js +93 -93
  13. package/__tests__/config.test.js +92 -92
  14. package/__tests__/formatter.test.js +63 -63
  15. package/__tests__/formatters.test.js +394 -394
  16. package/__tests__/migration.test.js +322 -322
  17. package/__tests__/semver.test.js +94 -94
  18. package/__tests__/tones.test.js +252 -252
  19. package/action.yml +113 -113
  20. package/index.js +73 -73
  21. package/package.json +47 -41
  22. package/src/ai-writer.js +108 -108
  23. package/src/analyzer.js +232 -232
  24. package/src/analyzers/migration.js +355 -355
  25. package/src/categorizer.js +182 -182
  26. package/src/config.js +162 -162
  27. package/src/constants.js +137 -137
  28. package/src/contributor.js +144 -144
  29. package/src/diff-analyzer.js +202 -202
  30. package/src/formatter.js +336 -336
  31. package/src/formatters/discord.js +174 -174
  32. package/src/formatters/html.js +195 -195
  33. package/src/formatters/index.js +42 -42
  34. package/src/formatters/markdown.js +123 -123
  35. package/src/formatters/slack.js +176 -176
  36. package/src/formatters/twitter.js +242 -242
  37. package/src/formatters/types.js +48 -48
  38. package/src/generator.js +297 -297
  39. package/src/integrations/changelog.js +125 -125
  40. package/src/integrations/discord.js +96 -96
  41. package/src/integrations/github-release.js +75 -75
  42. package/src/integrations/indexer.js +119 -119
  43. package/src/integrations/slack.js +112 -112
  44. package/src/integrations/twitter.js +128 -128
  45. package/src/logger.js +52 -52
  46. package/src/prompts.js +210 -210
  47. package/src/rate-limiter.js +92 -92
  48. package/src/semver.js +129 -129
  49. package/src/tones/casual.js +114 -114
  50. package/src/tones/humorous.js +164 -164
  51. package/src/tones/index.js +38 -38
  52. package/src/tones/professional.js +125 -125
  53. package/src/tones/technical.js +164 -164
  54. package/src/tones/types.js +26 -26
  55. package/jest.config.js +0 -10
package/src/generator.js CHANGED
@@ -1,297 +1,297 @@
1
- 'use strict';
2
-
3
- const github = require('@actions/github');
4
- const logger = require('./logger');
5
- const { analyzeCommits } = require('./analyzer');
6
- const { analyzeDiff, formatDiffStats } = require('./diff-analyzer');
7
- const { generateReleaseNotes: aiGenerate } = require('./ai-writer');
8
- const { formatReleaseNotes, generateSummary } = require('./formatter');
9
- const { postToIntegrations } = require('./integrations/indexer');
10
- const { parseVersion, isPrerelease } = require('./semver');
11
- const { createGitHubRateLimiter } = require('./rate-limiter');
12
-
13
- /**
14
- * Main orchestrator — generate release notes and post to integrations.
15
- * @param {Object} config - Validated configuration object
16
- * @returns {Object} { notes, url, version, summary }
17
- */
18
- async function generate(config) {
19
- logger.info('Starting release notes generation');
20
-
21
- const octokit = github.getOctokit(config.githubToken);
22
- const context = github.context;
23
- const rateLimiter = createGitHubRateLimiter();
24
-
25
- // Set repo info on config for formatter
26
- config.repoFullName = `${context.repo.owner}/${context.repo.repo}`;
27
- config.repoUrl = `https://github.com/${config.repoFullName}`;
28
-
29
- // Step 1: Determine version
30
- const version = determineVersion(config, context);
31
- logger.info(`Version: ${version}`);
32
-
33
- // Step 2: Determine previous version
34
- const previousVersion = await determinePreviousVersion(config, octokit, context, rateLimiter);
35
- logger.info(`Previous version: ${previousVersion || 'none (first release)'}`);
36
-
37
- // Step 3: Collect changes
38
- const { commits, prs } = await collectChanges(
39
- config, octokit, context, previousVersion, version, rateLimiter
40
- );
41
- logger.info(`Collected ${commits.length} commits and ${prs.length} PRs`);
42
-
43
- // Step 4: Analyze changes
44
- const analysis = analyzeCommits(commits, config);
45
- logger.info(`Analysis: ${analysis.stats.total_commits} commits in ${Object.keys(analysis.categories).length} categories`);
46
-
47
- // Step 5: Analyze diff (optional)
48
- let diffSummary = null;
49
- if (config.includeDiffStats) {
50
- try {
51
- await rateLimiter.wait();
52
- diffSummary = await analyzeDiff(commits.slice(0, Math.min(commits.length, 50)), octokit, context);
53
- } catch (err) {
54
- logger.warn('Diff analysis failed, continuing without it', err);
55
- diffSummary = { files_changed: 0, additions: 0, deletions: 0, impact: 'low', affected_areas: [], potential_breaking: [], files: [], files_added: 0, files_modified: 0, files_deleted: 0 };
56
- }
57
- }
58
-
59
- // Step 6: Generate release notes (AI or template)
60
- let notes;
61
- if (config.hasAI) {
62
- try {
63
- const aiNotes = await aiGenerate(analysis, diffSummary, version, config);
64
- if (aiNotes) {
65
- notes = aiNotes;
66
- logger.info('Using AI-generated release notes');
67
- } else {
68
- notes = formatReleaseNotes(analysis, diffSummary, version, previousVersion, config);
69
- logger.info('AI failed or returned nothing — using template-based notes');
70
- }
71
- } catch (err) {
72
- logger.warn('AI generation threw error — falling back to template', err);
73
- notes = formatReleaseNotes(analysis, diffSummary, version, previousVersion, config);
74
- }
75
- } else {
76
- notes = formatReleaseNotes(analysis, diffSummary, version, previousVersion, config);
77
- logger.info('Using template-based release notes (no API key)');
78
- }
79
-
80
- // Generate summary
81
- const summary = generateSummary(analysis, version);
82
-
83
- // Step 7: Post to integrations
84
- const releaseData = {
85
- version,
86
- notes,
87
- summary,
88
- url: '',
89
- contributors: analysis.contributors || [],
90
- };
91
-
92
- if (!config.dryRun) {
93
- try {
94
- const integrationResults = await postToIntegrations(config, releaseData, octokit, context);
95
- if (integrationResults.githubRelease && integrationResults.githubRelease.url) {
96
- releaseData.url = integrationResults.githubRelease.url;
97
- }
98
- } catch (err) {
99
- logger.error('Integration posting failed (non-fatal)', err);
100
- }
101
- } else {
102
- logger.info('Dry run mode — skipping all integrations');
103
- }
104
-
105
- logger.info(`Release notes generated for v${version}`);
106
-
107
- return {
108
- notes: releaseData.notes,
109
- url: releaseData.url,
110
- version: version,
111
- summary: summary,
112
- };
113
- }
114
-
115
- /**
116
- * Determine the current version based on config.
117
- */
118
- function determineVersion(config, context) {
119
- switch (config.versionFrom) {
120
- case 'tag': {
121
- const ref = process.env.GITHUB_REF || '';
122
- const tagMatch = ref.match(/refs\/tags\/v?(.+)/);
123
- if (tagMatch) {
124
- return tagMatch[1];
125
- }
126
- // Try to get from context payload
127
- if (context.payload.release && context.payload.release.tag_name) {
128
- return context.payload.release.tag_name.replace(/^v/, '');
129
- }
130
- logger.warn('Could not determine version from tag');
131
- return '0.0.0';
132
- }
133
- case 'package-json': {
134
- try {
135
- const fs = require('fs');
136
- const path = require('path');
137
- const pkgPath = path.join(process.env.GITHUB_WORKSPACE || process.cwd(), 'package.json');
138
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
139
- return pkg.version || '0.0.0';
140
- } catch (err) {
141
- logger.warn('Could not read version from package.json', err);
142
- return '0.0.0';
143
- }
144
- }
145
- case 'manual':
146
- return config.version || '0.0.0';
147
- default:
148
- return '0.0.0';
149
- }
150
- }
151
-
152
- /**
153
- * Determine the previous version/tag.
154
- */
155
- async function determinePreviousVersion(config, octokit, context, rateLimiter) {
156
- // Manual override
157
- if (config.previousTag) {
158
- return config.previousTag.replace(/^v/, '');
159
- }
160
-
161
- // Auto-detect from GitHub releases
162
- try {
163
- await rateLimiter.wait();
164
- const { data: releases } = await octokit.rest.repos.listReleases({
165
- owner: context.repo.owner,
166
- repo: context.repo.repo,
167
- per_page: 10,
168
- });
169
-
170
- for (const release of releases) {
171
- const tag = release.tag_name.replace(/^v/, '');
172
- if (!isPrerelease(tag) || release.prerelease) {
173
- // Skip prereleases for finding previous stable
174
- if (!release.prerelease) {
175
- return tag;
176
- }
177
- }
178
- }
179
-
180
- // If no stable release found, use the latest release of any type
181
- if (releases.length > 0) {
182
- return releases[0].tag_name.replace(/^v/, '');
183
- }
184
- } catch (err) {
185
- logger.debug(`Could not auto-detect previous version: ${err.message}`);
186
- }
187
-
188
- return '';
189
- }
190
-
191
- /**
192
- * Collect changes (commits and/or PRs) between versions.
193
- */
194
- async function collectChanges(config, octokit, context, previousVersion, currentVersion, rateLimiter) {
195
- const owner = context.repo.owner;
196
- const repo = context.repo.repo;
197
- let commits = [];
198
- let prs = [];
199
-
200
- const commitMode = config.commitMode;
201
-
202
- // Determine if we should use PR mode
203
- let usePRs = false;
204
- if (commitMode === 'pull-requests') {
205
- usePRs = true;
206
- } else if (commitMode === 'auto') {
207
- // Check if there are PRs between the versions
208
- try {
209
- await rateLimiter.wait();
210
- const base = previousVersion ? `v${previousVersion}` : undefined;
211
- const head = `v${currentVersion}`;
212
- if (base) {
213
- const { data: compareData } = await octokit.rest.repos.compareCommits({
214
- owner,
215
- repo,
216
- base,
217
- head,
218
- });
219
- // If commits have PR numbers, prefer PR mode
220
- const commitsWithPRs = (compareData.commits || []).filter(
221
- c => c.commit.message.match(/\(#\d+\)/)
222
- );
223
- usePRs = commitsWithPRs.length > (compareData.commits || []).length * 0.3;
224
- }
225
- } catch (err) {
226
- logger.debug(`Could not determine auto mode: ${err.message}`);
227
- usePRs = false;
228
- }
229
- }
230
-
231
- if (usePRs) {
232
- // Collect merged PRs between versions
233
- try {
234
- await rateLimiter.wait();
235
- const prQuery = previousVersion
236
- ? `is:pr is:merged repo:${owner}/${repo} base:main merged:>="${new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]}"`
237
- : `is:pr is:merged repo:${owner}/${repo}`;
238
-
239
- // Use search API for PRs
240
- const { data: searchResult } = await octokit.rest.search.issuesAndPullRequests({
241
- q: prQuery,
242
- sort: 'updated',
243
- order: 'desc',
244
- per_page: Math.min(config.maxCommits, 100),
245
- });
246
-
247
- prs = searchResult.items || [];
248
- commits = prs.map(pr => ({
249
- sha: pr.merge_commit_sha || '',
250
- message: pr.title || '',
251
- author: pr.user || {},
252
- url: pr.html_url,
253
- _pr: pr.number,
254
- }));
255
- } catch (err) {
256
- logger.warn(`PR collection failed, falling back to commits: ${err.message}`);
257
- usePRs = false;
258
- }
259
- }
260
-
261
- if (!usePRs) {
262
- // Collect commits between versions
263
- try {
264
- if (previousVersion) {
265
- await rateLimiter.wait();
266
- const { data: compareData } = await octokit.rest.repos.compareCommits({
267
- owner,
268
- repo,
269
- base: `v${previousVersion}`,
270
- head: `v${currentVersion}`,
271
- });
272
- commits = (compareData.commits || []).slice(0, config.maxCommits);
273
- } else {
274
- // No previous version — get recent commits
275
- await rateLimiter.wait();
276
- const { data: commitList } = await octokit.rest.repos.listCommits({
277
- owner,
278
- repo,
279
- per_page: Math.min(config.maxCommits, 100),
280
- });
281
- commits = commitList;
282
- }
283
- } catch (err) {
284
- logger.warn(`Commit collection failed: ${err.message}`);
285
- commits = [];
286
- }
287
- }
288
-
289
- // Handle no changes
290
- if (commits.length === 0 && prs.length === 0) {
291
- logger.info('No changes found between versions');
292
- }
293
-
294
- return { commits, prs };
295
- }
296
-
297
- module.exports = { generate, determineVersion, determinePreviousVersion, collectChanges };
1
+ 'use strict';
2
+
3
+ const github = require('@actions/github');
4
+ const logger = require('./logger');
5
+ const { analyzeCommits } = require('./analyzer');
6
+ const { analyzeDiff, formatDiffStats } = require('./diff-analyzer');
7
+ const { generateReleaseNotes: aiGenerate } = require('./ai-writer');
8
+ const { formatReleaseNotes, generateSummary } = require('./formatter');
9
+ const { postToIntegrations } = require('./integrations/indexer');
10
+ const { parseVersion, isPrerelease } = require('./semver');
11
+ const { createGitHubRateLimiter } = require('./rate-limiter');
12
+
13
+ /**
14
+ * Main orchestrator — generate release notes and post to integrations.
15
+ * @param {Object} config - Validated configuration object
16
+ * @returns {Object} { notes, url, version, summary }
17
+ */
18
+ async function generate(config) {
19
+ logger.info('Starting release notes generation');
20
+
21
+ const octokit = github.getOctokit(config.githubToken);
22
+ const context = github.context;
23
+ const rateLimiter = createGitHubRateLimiter();
24
+
25
+ // Set repo info on config for formatter
26
+ config.repoFullName = `${context.repo.owner}/${context.repo.repo}`;
27
+ config.repoUrl = `https://github.com/${config.repoFullName}`;
28
+
29
+ // Step 1: Determine version
30
+ const version = determineVersion(config, context);
31
+ logger.info(`Version: ${version}`);
32
+
33
+ // Step 2: Determine previous version
34
+ const previousVersion = await determinePreviousVersion(config, octokit, context, rateLimiter);
35
+ logger.info(`Previous version: ${previousVersion || 'none (first release)'}`);
36
+
37
+ // Step 3: Collect changes
38
+ const { commits, prs } = await collectChanges(
39
+ config, octokit, context, previousVersion, version, rateLimiter
40
+ );
41
+ logger.info(`Collected ${commits.length} commits and ${prs.length} PRs`);
42
+
43
+ // Step 4: Analyze changes
44
+ const analysis = analyzeCommits(commits, config);
45
+ logger.info(`Analysis: ${analysis.stats.total_commits} commits in ${Object.keys(analysis.categories).length} categories`);
46
+
47
+ // Step 5: Analyze diff (optional)
48
+ let diffSummary = null;
49
+ if (config.includeDiffStats) {
50
+ try {
51
+ await rateLimiter.wait();
52
+ diffSummary = await analyzeDiff(commits.slice(0, Math.min(commits.length, 50)), octokit, context);
53
+ } catch (err) {
54
+ logger.warn('Diff analysis failed, continuing without it', err);
55
+ diffSummary = { files_changed: 0, additions: 0, deletions: 0, impact: 'low', affected_areas: [], potential_breaking: [], files: [], files_added: 0, files_modified: 0, files_deleted: 0 };
56
+ }
57
+ }
58
+
59
+ // Step 6: Generate release notes (AI or template)
60
+ let notes;
61
+ if (config.hasAI) {
62
+ try {
63
+ const aiNotes = await aiGenerate(analysis, diffSummary, version, config);
64
+ if (aiNotes) {
65
+ notes = aiNotes;
66
+ logger.info('Using AI-generated release notes');
67
+ } else {
68
+ notes = formatReleaseNotes(analysis, diffSummary, version, previousVersion, config);
69
+ logger.info('AI failed or returned nothing — using template-based notes');
70
+ }
71
+ } catch (err) {
72
+ logger.warn('AI generation threw error — falling back to template', err);
73
+ notes = formatReleaseNotes(analysis, diffSummary, version, previousVersion, config);
74
+ }
75
+ } else {
76
+ notes = formatReleaseNotes(analysis, diffSummary, version, previousVersion, config);
77
+ logger.info('Using template-based release notes (no API key)');
78
+ }
79
+
80
+ // Generate summary
81
+ const summary = generateSummary(analysis, version);
82
+
83
+ // Step 7: Post to integrations
84
+ const releaseData = {
85
+ version,
86
+ notes,
87
+ summary,
88
+ url: '',
89
+ contributors: analysis.contributors || [],
90
+ };
91
+
92
+ if (!config.dryRun) {
93
+ try {
94
+ const integrationResults = await postToIntegrations(config, releaseData, octokit, context);
95
+ if (integrationResults.githubRelease && integrationResults.githubRelease.url) {
96
+ releaseData.url = integrationResults.githubRelease.url;
97
+ }
98
+ } catch (err) {
99
+ logger.error('Integration posting failed (non-fatal)', err);
100
+ }
101
+ } else {
102
+ logger.info('Dry run mode — skipping all integrations');
103
+ }
104
+
105
+ logger.info(`Release notes generated for v${version}`);
106
+
107
+ return {
108
+ notes: releaseData.notes,
109
+ url: releaseData.url,
110
+ version: version,
111
+ summary: summary,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Determine the current version based on config.
117
+ */
118
+ function determineVersion(config, context) {
119
+ switch (config.versionFrom) {
120
+ case 'tag': {
121
+ const ref = process.env.GITHUB_REF || '';
122
+ const tagMatch = ref.match(/refs\/tags\/v?(.+)/);
123
+ if (tagMatch) {
124
+ return tagMatch[1];
125
+ }
126
+ // Try to get from context payload
127
+ if (context.payload.release && context.payload.release.tag_name) {
128
+ return context.payload.release.tag_name.replace(/^v/, '');
129
+ }
130
+ logger.warn('Could not determine version from tag');
131
+ return '0.0.0';
132
+ }
133
+ case 'package-json': {
134
+ try {
135
+ const fs = require('fs');
136
+ const path = require('path');
137
+ const pkgPath = path.join(process.env.GITHUB_WORKSPACE || process.cwd(), 'package.json');
138
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
139
+ return pkg.version || '0.0.0';
140
+ } catch (err) {
141
+ logger.warn('Could not read version from package.json', err);
142
+ return '0.0.0';
143
+ }
144
+ }
145
+ case 'manual':
146
+ return config.version || '0.0.0';
147
+ default:
148
+ return '0.0.0';
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Determine the previous version/tag.
154
+ */
155
+ async function determinePreviousVersion(config, octokit, context, rateLimiter) {
156
+ // Manual override
157
+ if (config.previousTag) {
158
+ return config.previousTag.replace(/^v/, '');
159
+ }
160
+
161
+ // Auto-detect from GitHub releases
162
+ try {
163
+ await rateLimiter.wait();
164
+ const { data: releases } = await octokit.rest.repos.listReleases({
165
+ owner: context.repo.owner,
166
+ repo: context.repo.repo,
167
+ per_page: 10,
168
+ });
169
+
170
+ for (const release of releases) {
171
+ const tag = release.tag_name.replace(/^v/, '');
172
+ if (!isPrerelease(tag) || release.prerelease) {
173
+ // Skip prereleases for finding previous stable
174
+ if (!release.prerelease) {
175
+ return tag;
176
+ }
177
+ }
178
+ }
179
+
180
+ // If no stable release found, use the latest release of any type
181
+ if (releases.length > 0) {
182
+ return releases[0].tag_name.replace(/^v/, '');
183
+ }
184
+ } catch (err) {
185
+ logger.debug(`Could not auto-detect previous version: ${err.message}`);
186
+ }
187
+
188
+ return '';
189
+ }
190
+
191
+ /**
192
+ * Collect changes (commits and/or PRs) between versions.
193
+ */
194
+ async function collectChanges(config, octokit, context, previousVersion, currentVersion, rateLimiter) {
195
+ const owner = context.repo.owner;
196
+ const repo = context.repo.repo;
197
+ let commits = [];
198
+ let prs = [];
199
+
200
+ const commitMode = config.commitMode;
201
+
202
+ // Determine if we should use PR mode
203
+ let usePRs = false;
204
+ if (commitMode === 'pull-requests') {
205
+ usePRs = true;
206
+ } else if (commitMode === 'auto') {
207
+ // Check if there are PRs between the versions
208
+ try {
209
+ await rateLimiter.wait();
210
+ const base = previousVersion ? `v${previousVersion}` : undefined;
211
+ const head = `v${currentVersion}`;
212
+ if (base) {
213
+ const { data: compareData } = await octokit.rest.repos.compareCommits({
214
+ owner,
215
+ repo,
216
+ base,
217
+ head,
218
+ });
219
+ // If commits have PR numbers, prefer PR mode
220
+ const commitsWithPRs = (compareData.commits || []).filter(
221
+ c => c.commit.message.match(/\(#\d+\)/)
222
+ );
223
+ usePRs = commitsWithPRs.length > (compareData.commits || []).length * 0.3;
224
+ }
225
+ } catch (err) {
226
+ logger.debug(`Could not determine auto mode: ${err.message}`);
227
+ usePRs = false;
228
+ }
229
+ }
230
+
231
+ if (usePRs) {
232
+ // Collect merged PRs between versions
233
+ try {
234
+ await rateLimiter.wait();
235
+ const prQuery = previousVersion
236
+ ? `is:pr is:merged repo:${owner}/${repo} base:main merged:>="${new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]}"`
237
+ : `is:pr is:merged repo:${owner}/${repo}`;
238
+
239
+ // Use search API for PRs
240
+ const { data: searchResult } = await octokit.rest.search.issuesAndPullRequests({
241
+ q: prQuery,
242
+ sort: 'updated',
243
+ order: 'desc',
244
+ per_page: Math.min(config.maxCommits, 100),
245
+ });
246
+
247
+ prs = searchResult.items || [];
248
+ commits = prs.map(pr => ({
249
+ sha: pr.merge_commit_sha || '',
250
+ message: pr.title || '',
251
+ author: pr.user || {},
252
+ url: pr.html_url,
253
+ _pr: pr.number,
254
+ }));
255
+ } catch (err) {
256
+ logger.warn(`PR collection failed, falling back to commits: ${err.message}`);
257
+ usePRs = false;
258
+ }
259
+ }
260
+
261
+ if (!usePRs) {
262
+ // Collect commits between versions
263
+ try {
264
+ if (previousVersion) {
265
+ await rateLimiter.wait();
266
+ const { data: compareData } = await octokit.rest.repos.compareCommits({
267
+ owner,
268
+ repo,
269
+ base: `v${previousVersion}`,
270
+ head: `v${currentVersion}`,
271
+ });
272
+ commits = (compareData.commits || []).slice(0, config.maxCommits);
273
+ } else {
274
+ // No previous version — get recent commits
275
+ await rateLimiter.wait();
276
+ const { data: commitList } = await octokit.rest.repos.listCommits({
277
+ owner,
278
+ repo,
279
+ per_page: Math.min(config.maxCommits, 100),
280
+ });
281
+ commits = commitList;
282
+ }
283
+ } catch (err) {
284
+ logger.warn(`Commit collection failed: ${err.message}`);
285
+ commits = [];
286
+ }
287
+ }
288
+
289
+ // Handle no changes
290
+ if (commits.length === 0 && prs.length === 0) {
291
+ logger.info('No changes found between versions');
292
+ }
293
+
294
+ return { commits, prs };
295
+ }
296
+
297
+ module.exports = { generate, determineVersion, determinePreviousVersion, collectChanges };