forked from hiero-ledger/hiero-sdk-python
-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathbot-conventional-pr-title.js
More file actions
313 lines (268 loc) Β· 10.5 KB
/
Copy pathbot-conventional-pr-title.js
File metadata and controls
313 lines (268 loc) Β· 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
/**
* Bot that provides automated guidance for PR titles.
*
* Provides automated suggestions for fixing non-conventional PR titles.
*
* @module bot-conventional-pr-title
*/
const MAX_COMMENTS_TO_FETCH = 500;
const COMMENT_IDENTIFIER = '<!-- bot-conventional-pr-title -->';
/**
* Suggest appropriate conventional commit type based on PR title keywords
* @param {string} title - The PR title to analyze
* @returns {string} The suggested conventional commit type
*/
function suggestConventionalType(title) {
console.log('[Bot] Analyzing title for type suggestion:', title);
if (!title || typeof title !== 'string') {
console.log('[Bot] β οΈ Invalid title, defaulting to "chore"');
return 'chore';
}
const lowerTitle = title.toLowerCase();
// Keyword patterns mapped to conventional types (priority order)
const typePatterns = [
{ type: 'style', pattern: /\b(format|formatting|style|prettier|eslint|lint)\b/, label: 'formatting' },
{ type: 'docs', pattern: /\b(docs|documentation|readme|comment|comments)\b/, label: 'documentation' },
{ type: 'fix', pattern: /\b(fix|bug|issue|error|crash|problem)\b/, label: 'bug fix' },
{ type: 'test', pattern: /\b(test|testing|spec|unit|integration)\b/, label: 'test' },
{ type: 'refactor', pattern: /\b(refactor|refactoring|restructure|reorganize)\b/, label: 'refactor' },
{ type: 'perf', pattern: /\b(perf|performance|optimize|speed)\b/, label: 'performance' },
{ type: 'build', pattern: /\b(build|compile|dependency|dependencies|deps)\b/, label: 'build' },
{ type: 'ci', pattern: /\b(ci|workflow|action|pipeline)\b/, label: 'CI' },
{ type: 'revert', pattern: /\b(revert|reverts|reverting|rollback|undo)\b/, label: 'revert' },
{ type: 'feat', pattern: /\b(add|adds|added|feature|features|new|implement|implements|implemented|introduce|introduced)\b/, label: 'feature' }
];
// Check each pattern in priority order
for (const { type, pattern, label } of typePatterns) {
if (lowerTitle.match(pattern)) {
console.log(`[Bot] Detected ${label} keywords β suggesting "${type}"`);
return type;
}
}
console.log('[Bot] No specific keywords matched β suggesting "chore"');
return 'chore';
}
/**
* Generate the header section of the bot message
* @param {string} safeTitle - Current PR title (markdown-escaped)
* @param {string} suggestedType - Suggested conventional type
* @returns {string} Formatted header
*/
function generateMessageHeader(safeTitle, suggestedType) {
return `${COMMENT_IDENTIFIER}
## PR Title Needs Conventional Format
**Your current title is:**
\`\`\`
${safeTitle}
\`\`\`
**It needs to have a type prefix like:**
\`\`\`
${suggestedType}: ${safeTitle}
\`\`\`
---
`;
}
/**
* Generate the fix instructions section
* @param {string} suggestedType - Suggested conventional type
* @param {string} escapedTitle - Shell-escaped title
* @param {number} prNumber - PR number
* @returns {string} Formatted instructions
*/
function generateFixInstructions(suggestedType, escapedTitle, prNumber) {
return `### How to Fix This
#### Option 1: Via GitHub UI
1. Go to the top of this PR page
2. Click the **edit button** (βοΈ) next to the PR title at the top of the page
3. Add the type prefix (e.g., \`${suggestedType}:\`) before your current title
4. Save the changes
#### Option 2: Via Command Line
\`\`\`bash
# Note: Adjust the title as needed if it contains special characters
gh pr edit ${prNumber} --title "${suggestedType}: ${escapedTitle}"
\`\`\`
---
`;
}
/**
* Generate the valid types reference section
* @returns {string} Formatted types list
*/
function generateValidTypesList() {
return `### Valid Conventional Commit Types
- \`feat\` - New feature
- \`fix\` - Bug fix
- \`docs\` - Documentation changes
- \`style\` - Code style changes (formatting, missing semi-colons, etc)
- \`refactor\` - Code refactoring
- \`perf\` - Performance improvements
- \`test\` - Adding or updating tests
- \`build\` - Build system changes
- \`ci\` - CI configuration changes
- \`chore\` - Other changes that don't modify src or test files
- \`revert\` - Reverts a previous commit
π Learn more: [Conventional Commits](https://www.conventionalcommits.org/)
`;
}
/**
* Escape user-provided text for safe markdown display
* Prevents markdown injection without altering meaning
* @param {string} text
* @returns {string}
*/
function escapeForMarkdown(text) {
return text
.replace(/```/g, "'''")
.replace(/\r?\n|\r/g, ' ')
.trim();
}
/**
* Compose the complete bot message
* @param {Object} params - Message parameters
* @param {string} params.safeTitle - Markdown-escaped title
* @param {string} params.escapedTitle - Shell-escaped title
* @param {string} params.suggestedType - Suggested conventional type
* @param {number} params.prNumber - PR number
* @returns {string} Complete formatted message
*/
function composeBotMessage({ safeTitle, escapedTitle, suggestedType, prNumber }) {
return (
generateMessageHeader(safeTitle, suggestedType) +
generateFixInstructions(suggestedType, escapedTitle, prNumber) +
generateValidTypesList()
);
}
/**
* Format the bot comment message with title guidance
* @param {string} currentTitle - The current PR title
* @param {string} suggestedType - The suggested conventional type
* @param {number} prNumber - The PR number
* @returns {string} Formatted markdown message
*/
function formatMessage(currentTitle, suggestedType, prNumber) {
// Escape shell-sensitive characters for the CLI example
const escapedTitle = currentTitle.replace(/["$`\\]/g, '\\$&');
const safeTitle = escapeForMarkdown(currentTitle);
return composeBotMessage({
safeTitle,
escapedTitle,
suggestedType,
prNumber,
});
}
/**
* Main bot execution function
* @param {Object} params - Function parameters
* @param {Object} params.github - GitHub API client
* @param {Object} params.context - GitHub Actions context
* @param {number} params.prNumber - Pull request number
* @param {string} params.prTitle - Pull request title
* @param {boolean} [params.dryRun=false] - Dry run mode flag
* @returns {Promise<void>}
*/
async function run({ github, context, prNumber, prTitle, dryRun = false }) {
try {
console.log('='.repeat(60));
console.log('[Bot] Starting conventional PR title bot');
console.log('[Bot] Dry Run Mode:', dryRun);
console.log('[Bot] PR Number:', prNumber);
console.log('[Bot] PR Title:', prTitle);
console.log('[Bot] Repository:', `${context.repo.owner}/${context.repo.repo}`);
console.log('='.repeat(60));
// Validate inputs
if (!prNumber || typeof prNumber !== 'number') {
console.error('[Bot] β Invalid PR number:', prNumber);
throw new Error('Invalid PR number provided');
}
if (!prTitle || typeof prTitle !== 'string') {
console.error('[Bot] β Invalid PR title:', prTitle);
throw new Error('Invalid PR title provided');
}
// Suggest appropriate conventional type
const suggestedType = suggestConventionalType(prTitle);
// Format the bot message
const message = formatMessage(prTitle, suggestedType, prNumber);
console.log('[Bot] Fetching PR comments with pagination...');
// Fetch comments with pagination and early exit
let commentCount = 0;
let page = 1;
let botComment = null;
while (commentCount < MAX_COMMENTS_TO_FETCH && !botComment) {
const response = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
page: page
});
commentCount += response.data.length;
// Check if we found the bot comment
botComment = response.data.find(comment =>
comment.body && comment.body.includes(COMMENT_IDENTIFIER)
);
// Exit if no more pages or found the comment
if (response.data.length < 100 || botComment) {
break;
}
page++;
}
console.log(`[Bot] Fetched ${commentCount} comments across ${page} page(s)`);
if (dryRun) {
console.log('='.repeat(60));
console.log('[Bot] π DRY RUN MODE - No changes will be made');
console.log('[Bot] Would suggest type:', suggestedType);
console.log('[Bot] Bot comment exists:', !!botComment);
console.log('[Bot] Action that would be taken:', botComment ? 'UPDATE' : 'CREATE');
console.log('='.repeat(60));
return;
}
if (botComment) {
console.log('[Bot] Found existing bot comment, updating...');
console.log('[Bot] Comment ID:', botComment.id);
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: message,
});
console.log('[Bot] β
Successfully updated existing comment');
} else {
console.log('[Bot] No existing bot comment found, creating new one...');
const response = await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: message,
});
console.log('[Bot] β
Successfully created new comment');
console.log('[Bot] Comment ID:', response.data.id);
}
console.log('='.repeat(60));
console.log('[Bot] Bot execution completed successfully');
console.log('='.repeat(60));
} catch (error) {
console.error('='.repeat(60));
console.error('[Bot] β Error occurred during bot execution');
// Handle permission errors gracefully
if (error.status === 403) {
console.error('[Bot] Permission denied - bot lacks write access to this repository');
console.error('[Bot] This is expected for fork PRs and will not fail the workflow');
console.error('[Bot] The bot will function normally once the PR is on the main repository');
console.error('='.repeat(60));
return; // Exit gracefully, don't fail the workflow
}
// Log other errors with details
console.error('[Bot] Error name:', error.name);
console.error('[Bot] Error message:', error.message);
console.error('[Bot] Error status:', error.status);
console.error('[Bot] Error stack:', error.stack);
console.error('='.repeat(60));
throw error;
}
}
// Export functions for testing and workflow usage
module.exports = {
run,
suggestConventionalType,
formatMessage
};