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