@happyvertical/github-actions 0.80.0 → 0.80.2
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/dist/cli/claude-context.js +17 -17
- package/dist/cli/claude-context.js.map +1 -1
- package/dist/cli.js +227 -232
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2 -637
- package/dist/src-CezXcNYi.js +1318 -0
- package/dist/src-CezXcNYi.js.map +1 -0
- package/package.json +6 -6
- package/dist/index-v2-CqFKwTm8.js +0 -687
- package/dist/index-v2-CqFKwTm8.js.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,1318 @@
|
|
|
1
|
+
import { getProject } from "@happyvertical/projects";
|
|
2
|
+
import { getRepository } from "@happyvertical/repos";
|
|
3
|
+
import https from "node:https";
|
|
4
|
+
//#region \0rolldown/runtime.js
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __exportAll = (all, no_symbols) => {
|
|
7
|
+
let target = {};
|
|
8
|
+
for (var name in all) __defProp(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true
|
|
11
|
+
});
|
|
12
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
return target;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/shared/adapters.ts
|
|
17
|
+
/**
|
|
18
|
+
* Create a repository client from GitHub context
|
|
19
|
+
*/
|
|
20
|
+
async function createRepository(token, owner, repo) {
|
|
21
|
+
return getRepository({
|
|
22
|
+
type: "github",
|
|
23
|
+
owner,
|
|
24
|
+
repo,
|
|
25
|
+
token
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create a project client from configuration
|
|
30
|
+
*/
|
|
31
|
+
async function createProject(token, projectId, statusFieldId, statusOptions) {
|
|
32
|
+
return getProject({
|
|
33
|
+
type: "github",
|
|
34
|
+
projectId,
|
|
35
|
+
token,
|
|
36
|
+
statusFieldId,
|
|
37
|
+
statusOptions
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/planning/analyze.ts
|
|
42
|
+
/**
|
|
43
|
+
* Generate an AI-powered implementation plan for a GitHub issue.
|
|
44
|
+
*
|
|
45
|
+
* Builds a prompt from the issue context and repository description, then
|
|
46
|
+
* calls the AI service to produce a structured plan with tasks, complexity,
|
|
47
|
+
* and technical considerations.
|
|
48
|
+
*
|
|
49
|
+
* @param context - Planning context with issue details and repository config
|
|
50
|
+
* @returns Structured implementation plan
|
|
51
|
+
*/
|
|
52
|
+
async function analyzePlanning(context) {
|
|
53
|
+
const systemPrompt = `You are a software development planning assistant for the ${context.owner}/${context.repo} repository.
|
|
54
|
+
|
|
55
|
+
Repository: ${context.config.repoDescription}
|
|
56
|
+
${context.config.packagePattern ? `Package pattern: ${context.config.packagePattern}` : ""}
|
|
57
|
+
${context.config.packageExamples ? `Example packages: ${context.config.packageExamples.join(", ")}` : ""}
|
|
58
|
+
|
|
59
|
+
Your task is to create a detailed implementation plan for issues. Analyze the requirements and provide:
|
|
60
|
+
1. A clear summary of what needs to be done
|
|
61
|
+
2. A step-by-step task breakdown
|
|
62
|
+
3. Complexity assessment (simple, moderate, complex)
|
|
63
|
+
4. Technical considerations
|
|
64
|
+
5. Files likely to be affected
|
|
65
|
+
6. Dependencies or blockers`;
|
|
66
|
+
const userPrompt = `Issue #${context.issueNumber}: ${context.issueTitle}
|
|
67
|
+
|
|
68
|
+
${context.issueBody || "No description provided"}
|
|
69
|
+
|
|
70
|
+
Please create a detailed implementation plan following this JSON structure:
|
|
71
|
+
{
|
|
72
|
+
"summary": "Brief summary of the implementation",
|
|
73
|
+
"tasks": ["Step 1", "Step 2", "Step 3"],
|
|
74
|
+
"complexity": "simple|moderate|complex",
|
|
75
|
+
"considerations": ["Technical consideration 1", "Technical consideration 2"],
|
|
76
|
+
"affected_files": ["file1.ts", "file2.ts"],
|
|
77
|
+
"dependencies": ["Optional blocker or dependency"]
|
|
78
|
+
}`;
|
|
79
|
+
console.log("AI Planning Analysis would be called here");
|
|
80
|
+
console.log("System:", systemPrompt);
|
|
81
|
+
console.log("User:", userPrompt);
|
|
82
|
+
return {
|
|
83
|
+
summary: `Implementation plan for: ${context.issueTitle}`,
|
|
84
|
+
tasks: [
|
|
85
|
+
"Analyze requirements and design approach",
|
|
86
|
+
"Implement core functionality",
|
|
87
|
+
"Add tests",
|
|
88
|
+
"Update documentation"
|
|
89
|
+
],
|
|
90
|
+
complexity: "moderate",
|
|
91
|
+
considerations: [
|
|
92
|
+
"Ensure backward compatibility",
|
|
93
|
+
"Follow existing code patterns",
|
|
94
|
+
"Add comprehensive error handling"
|
|
95
|
+
],
|
|
96
|
+
affected_files: [],
|
|
97
|
+
dependencies: []
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/planning/definition-of-ready.ts
|
|
102
|
+
/**
|
|
103
|
+
* Check if an issue meets the Definition of Ready criteria
|
|
104
|
+
*/
|
|
105
|
+
function validateDefinitionOfReady(issue, hasPlanComment) {
|
|
106
|
+
const labels = issue.labels.map((l) => typeof l === "string" ? l : l.name);
|
|
107
|
+
return {
|
|
108
|
+
hasClearDescription: !!issue.body && issue.body.trim().length > 50,
|
|
109
|
+
hasTypeLabel: labels.some((l) => l.startsWith("type:")),
|
|
110
|
+
hasPriorityLabel: labels.some((l) => l.startsWith("priority:")),
|
|
111
|
+
hasSizeLabel: labels.some((l) => l.startsWith("size:")),
|
|
112
|
+
hasPlan: hasPlanComment,
|
|
113
|
+
noBlockers: !labels.some((l) => l === "status: blocked")
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Check if issue is ready (all criteria met)
|
|
118
|
+
*/
|
|
119
|
+
function isReady(dor) {
|
|
120
|
+
return dor.hasClearDescription && dor.hasTypeLabel && dor.hasPriorityLabel && dor.hasSizeLabel && dor.hasPlan && dor.noBlockers;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Format Definition of Ready as markdown checklist
|
|
124
|
+
*/
|
|
125
|
+
function formatDefinitionOfReady(dor) {
|
|
126
|
+
const checkbox = (checked) => checked ? "[x]" : "[ ]";
|
|
127
|
+
return `## Definition of Ready
|
|
128
|
+
|
|
129
|
+
${checkbox(dor.hasClearDescription)} Clear, actionable description
|
|
130
|
+
${checkbox(dor.hasTypeLabel)} Type label applied
|
|
131
|
+
${checkbox(dor.hasPriorityLabel)} Priority label applied
|
|
132
|
+
${checkbox(dor.hasSizeLabel)} Size label applied
|
|
133
|
+
${checkbox(dor.hasPlan)} Implementation plan documented
|
|
134
|
+
${checkbox(dor.noBlockers)} No blocking dependencies
|
|
135
|
+
|
|
136
|
+
${isReady(dor) ? "✅ **This issue is ready for implementation!**" : "⚠️ **This issue needs more work before it's ready.**"}`;
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/planning/comment.ts
|
|
140
|
+
/**
|
|
141
|
+
* Post implementation plan comment
|
|
142
|
+
*/
|
|
143
|
+
async function postPlanComment(repo, issueNumber, plan) {
|
|
144
|
+
const comment = `## 🤖 Implementation Plan
|
|
145
|
+
|
|
146
|
+
### Summary
|
|
147
|
+
${plan.summary}
|
|
148
|
+
|
|
149
|
+
### Tasks
|
|
150
|
+
${plan.tasks.map((task, i) => `${i + 1}. ${task}`).join("\n")}
|
|
151
|
+
|
|
152
|
+
### Complexity
|
|
153
|
+
**${plan.complexity.charAt(0).toUpperCase() + plan.complexity.slice(1)}**
|
|
154
|
+
|
|
155
|
+
### Technical Considerations
|
|
156
|
+
${plan.considerations.map((c) => `- ${c}`).join("\n")}
|
|
157
|
+
|
|
158
|
+
${plan.affected_files && plan.affected_files.length > 0 ? `### Affected Files\n${plan.affected_files.map((f) => `- \`${f}\``).join("\n")}` : ""}
|
|
159
|
+
|
|
160
|
+
${plan.dependencies && plan.dependencies.length > 0 ? `### Dependencies\n${plan.dependencies.map((d) => `- ${d}`).join("\n")}` : ""}
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
*This plan was generated by AI. Please review and provide feedback in the comments.*`;
|
|
164
|
+
await repo.addComment(issueNumber, comment);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Post Definition of Ready status comment
|
|
168
|
+
*/
|
|
169
|
+
async function postReadyCheckComment(repo, issueNumber, dor) {
|
|
170
|
+
const comment = formatDefinitionOfReady(dor);
|
|
171
|
+
await repo.addComment(issueNumber, comment);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Post planning error comment
|
|
175
|
+
*/
|
|
176
|
+
async function postPlanningErrorComment(repo, issueNumber, error) {
|
|
177
|
+
const comment = `## ⚠️ Planning Failed
|
|
178
|
+
|
|
179
|
+
An error occurred while creating the implementation plan:
|
|
180
|
+
|
|
181
|
+
\`\`\`
|
|
182
|
+
${error.message}
|
|
183
|
+
\`\`\`
|
|
184
|
+
|
|
185
|
+
Please try again or create a manual implementation plan in the comments.`;
|
|
186
|
+
await repo.addComment(issueNumber, comment);
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/planning/index.ts
|
|
190
|
+
/**
|
|
191
|
+
* Planning Workflow Orchestration
|
|
192
|
+
*/
|
|
193
|
+
/**
|
|
194
|
+
* Start planning workflow for an issue
|
|
195
|
+
*/
|
|
196
|
+
async function startPlanning(context) {
|
|
197
|
+
console.log(`Starting planning for issue #${context.issueNumber}: ${context.issueTitle}`);
|
|
198
|
+
try {
|
|
199
|
+
const repo = await createRepository(context.token, context.owner, context.repo);
|
|
200
|
+
await repo.addLabels(context.issueNumber, ["agent: planning"]);
|
|
201
|
+
console.log("Generating implementation plan...");
|
|
202
|
+
const plan = await analyzePlanning(context);
|
|
203
|
+
await postPlanComment(repo, context.issueNumber, plan);
|
|
204
|
+
console.log("✅ Planning started successfully");
|
|
205
|
+
return {
|
|
206
|
+
success: true,
|
|
207
|
+
plan
|
|
208
|
+
};
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error("❌ Planning failed:", error.message);
|
|
211
|
+
try {
|
|
212
|
+
await postPlanningErrorComment(await createRepository(context.token, context.owner, context.repo), context.issueNumber, error);
|
|
213
|
+
} catch {}
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
error: error.message
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Complete planning workflow and move to Ready
|
|
222
|
+
*/
|
|
223
|
+
async function completePlanning(context) {
|
|
224
|
+
console.log(`Completing planning for issue #${context.issueNumber}: ${context.issueTitle}`);
|
|
225
|
+
try {
|
|
226
|
+
const repo = await createRepository(context.token, context.owner, context.repo);
|
|
227
|
+
const issue = await repo.getIssue(context.issueNumber);
|
|
228
|
+
const dor = validateDefinitionOfReady(issue, (await repo.listComments(context.issueNumber)).some((c) => c.body?.includes("Implementation Plan")));
|
|
229
|
+
await postReadyCheckComment(repo, context.issueNumber, dor);
|
|
230
|
+
if (!isReady(dor)) {
|
|
231
|
+
console.log("⚠️ Issue does not meet Definition of Ready criteria");
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
error: "Issue does not meet Definition of Ready criteria"
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
await repo.removeLabel(context.issueNumber, "agent: planning");
|
|
238
|
+
if (context.config.projectId && context.config.statusFieldId && context.config.statusOptions) {
|
|
239
|
+
const project = await createProject(context.token, context.config.projectId, context.config.statusFieldId, context.config.statusOptions);
|
|
240
|
+
const itemId = (await project.listItems()).find((item) => item.contentId === issue.id)?.id;
|
|
241
|
+
if (itemId) await project.updateItemStatus(itemId, "Ready");
|
|
242
|
+
}
|
|
243
|
+
console.log("✅ Planning completed, moved to Ready");
|
|
244
|
+
return { success: true };
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error("❌ Complete planning failed:", error.message);
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
error: error.message
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/shared/ai.ts
|
|
255
|
+
/**
|
|
256
|
+
* Call GitHub Models API (available to all GitHub users)
|
|
257
|
+
*/
|
|
258
|
+
async function callGitHubModels(token, messages, options = {}) {
|
|
259
|
+
const model = options.model || "gpt-4o-mini";
|
|
260
|
+
const temperature = options.temperature ?? .3;
|
|
261
|
+
const maxTokens = options.maxTokens ?? 1e3;
|
|
262
|
+
const response = await fetch(`https://models.inference.ai.azure.com/chat/completions`, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: {
|
|
265
|
+
Authorization: `Bearer ${token}`,
|
|
266
|
+
"Content-Type": "application/json"
|
|
267
|
+
},
|
|
268
|
+
body: JSON.stringify({
|
|
269
|
+
model,
|
|
270
|
+
messages,
|
|
271
|
+
temperature,
|
|
272
|
+
max_tokens: maxTokens
|
|
273
|
+
})
|
|
274
|
+
});
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
const error = await response.text();
|
|
277
|
+
throw new Error(`GitHub Models API error: ${response.status} ${response.statusText}\n${error}`);
|
|
278
|
+
}
|
|
279
|
+
return (await response.json()).choices[0].message.content;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Call OpenAI API directly
|
|
283
|
+
*/
|
|
284
|
+
async function callOpenAI(apiKey, messages, options = {}) {
|
|
285
|
+
const model = options.model || "gpt-4o-mini";
|
|
286
|
+
const temperature = options.temperature ?? .3;
|
|
287
|
+
const maxTokens = options.maxTokens ?? 1e3;
|
|
288
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
289
|
+
method: "POST",
|
|
290
|
+
headers: {
|
|
291
|
+
Authorization: `Bearer ${apiKey}`,
|
|
292
|
+
"Content-Type": "application/json"
|
|
293
|
+
},
|
|
294
|
+
body: JSON.stringify({
|
|
295
|
+
model,
|
|
296
|
+
messages,
|
|
297
|
+
temperature,
|
|
298
|
+
max_tokens: maxTokens
|
|
299
|
+
})
|
|
300
|
+
});
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
const error = await response.text();
|
|
303
|
+
throw new Error(`OpenAI API error: ${response.status} ${response.statusText}\n${error}`);
|
|
304
|
+
}
|
|
305
|
+
return (await response.json()).choices[0].message.content;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Call Anthropic Claude API
|
|
309
|
+
*/
|
|
310
|
+
async function callAnthropic(apiKey, messages, options = {}) {
|
|
311
|
+
const model = options.model || "claude-3-5-sonnet-20241022";
|
|
312
|
+
const temperature = options.temperature ?? .3;
|
|
313
|
+
const maxTokens = options.maxTokens ?? 1e3;
|
|
314
|
+
const systemMessage = messages.find((m) => m.role === "system");
|
|
315
|
+
const conversationMessages = messages.filter((m) => m.role !== "system");
|
|
316
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: {
|
|
319
|
+
"x-api-key": apiKey,
|
|
320
|
+
"anthropic-version": "2023-06-01",
|
|
321
|
+
"Content-Type": "application/json"
|
|
322
|
+
},
|
|
323
|
+
body: JSON.stringify({
|
|
324
|
+
model,
|
|
325
|
+
max_tokens: maxTokens,
|
|
326
|
+
temperature,
|
|
327
|
+
system: systemMessage?.content,
|
|
328
|
+
messages: conversationMessages
|
|
329
|
+
})
|
|
330
|
+
});
|
|
331
|
+
if (!response.ok) {
|
|
332
|
+
const error = await response.text();
|
|
333
|
+
throw new Error(`Anthropic API error: ${response.status} ${response.statusText}\n${error}`);
|
|
334
|
+
}
|
|
335
|
+
return (await response.json()).content[0].text;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Unified AI completion function
|
|
339
|
+
*
|
|
340
|
+
* Automatically selects provider based on available environment variables:
|
|
341
|
+
* - GITHUB_TOKEN → GitHub Models (default)
|
|
342
|
+
* - OPENAI_API_KEY → OpenAI
|
|
343
|
+
* - ANTHROPIC_API_KEY → Anthropic
|
|
344
|
+
*/
|
|
345
|
+
async function getAICompletion(messages, options = {}) {
|
|
346
|
+
if (process.env.GITHUB_TOKEN) return callGitHubModels(process.env.GITHUB_TOKEN, messages, options);
|
|
347
|
+
if (process.env.OPENAI_API_KEY) return callOpenAI(process.env.OPENAI_API_KEY, messages, options);
|
|
348
|
+
if (process.env.ANTHROPIC_API_KEY) return callAnthropic(process.env.ANTHROPIC_API_KEY, messages, options);
|
|
349
|
+
throw new Error("No AI API credentials found. Set GITHUB_TOKEN, OPENAI_API_KEY, or ANTHROPIC_API_KEY environment variable.");
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Parse JSON from AI response
|
|
353
|
+
*
|
|
354
|
+
* Handles markdown code blocks and extracts JSON
|
|
355
|
+
*/
|
|
356
|
+
function parseAIJson(response) {
|
|
357
|
+
let cleaned = response.trim();
|
|
358
|
+
if (cleaned.startsWith("```json")) cleaned = cleaned.slice(7);
|
|
359
|
+
else if (cleaned.startsWith("```")) cleaned = cleaned.slice(3);
|
|
360
|
+
if (cleaned.endsWith("```")) cleaned = cleaned.slice(0, -3);
|
|
361
|
+
cleaned = cleaned.trim();
|
|
362
|
+
try {
|
|
363
|
+
return JSON.parse(cleaned);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
throw new Error(`Failed to parse AI response as JSON: ${error.message}\n\nResponse: ${response}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/shared/github.ts
|
|
370
|
+
/**
|
|
371
|
+
* Make a GitHub REST API request
|
|
372
|
+
*/
|
|
373
|
+
async function githubRequest(context, method, path, body) {
|
|
374
|
+
const url = `https://api.github.com${path}`;
|
|
375
|
+
const headers = {
|
|
376
|
+
Authorization: `Bearer ${context.token}`,
|
|
377
|
+
Accept: "application/vnd.github+json",
|
|
378
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
379
|
+
"Content-Type": "application/json"
|
|
380
|
+
};
|
|
381
|
+
const response = await fetch(url, {
|
|
382
|
+
method,
|
|
383
|
+
headers,
|
|
384
|
+
body: body ? JSON.stringify(body) : void 0
|
|
385
|
+
});
|
|
386
|
+
if (!response.ok) {
|
|
387
|
+
const error = await response.text();
|
|
388
|
+
throw new Error(`GitHub API error: ${response.status} ${response.statusText}\n${error}`);
|
|
389
|
+
}
|
|
390
|
+
return response.json();
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Make a GitHub GraphQL API request
|
|
394
|
+
*/
|
|
395
|
+
async function githubGraphQL(token, query, variables) {
|
|
396
|
+
const response = await fetch("https://api.github.com/graphql", {
|
|
397
|
+
method: "POST",
|
|
398
|
+
headers: {
|
|
399
|
+
Authorization: `Bearer ${token}`,
|
|
400
|
+
"Content-Type": "application/json"
|
|
401
|
+
},
|
|
402
|
+
body: JSON.stringify({
|
|
403
|
+
query,
|
|
404
|
+
variables
|
|
405
|
+
})
|
|
406
|
+
});
|
|
407
|
+
if (!response.ok) {
|
|
408
|
+
const error = await response.text();
|
|
409
|
+
throw new Error(`GitHub GraphQL error: ${response.status} ${response.statusText}\n${error}`);
|
|
410
|
+
}
|
|
411
|
+
const result = await response.json();
|
|
412
|
+
if (result.errors) throw new Error(`GitHub GraphQL errors: ${result.errors.map((e) => e.message).join(", ")}`);
|
|
413
|
+
return result.data;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Add labels to an issue
|
|
417
|
+
*/
|
|
418
|
+
async function addLabels(context, issueNumber, labels) {
|
|
419
|
+
await githubRequest(context, "POST", `/repos/${context.owner}/${context.repo}/issues/${issueNumber}/labels`, { labels });
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Remove a label from an issue
|
|
423
|
+
*/
|
|
424
|
+
async function removeLabel(context, issueNumber, label) {
|
|
425
|
+
const encodedLabel = encodeURIComponent(label);
|
|
426
|
+
await githubRequest(context, "DELETE", `/repos/${context.owner}/${context.repo}/issues/${issueNumber}/labels/${encodedLabel}`);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Post a comment on an issue
|
|
430
|
+
*/
|
|
431
|
+
async function postComment(context, issueNumber, body) {
|
|
432
|
+
await githubRequest(context, "POST", `/repos/${context.owner}/${context.repo}/issues/${issueNumber}/comments`, { body });
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Assign users to an issue
|
|
436
|
+
*/
|
|
437
|
+
async function assignIssue(context, issueNumber, assignees) {
|
|
438
|
+
await githubRequest(context, "POST", `/repos/${context.owner}/${context.repo}/issues/${issueNumber}/assignees`, { assignees });
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Create or update a repository label
|
|
442
|
+
*/
|
|
443
|
+
async function createOrUpdateLabel(context, name, color, description) {
|
|
444
|
+
try {
|
|
445
|
+
await githubRequest(context, "PATCH", `/repos/${context.owner}/${context.repo}/labels/${encodeURIComponent(name)}`, {
|
|
446
|
+
color,
|
|
447
|
+
description
|
|
448
|
+
});
|
|
449
|
+
} catch (error) {
|
|
450
|
+
if (error.message.includes("404")) await githubRequest(context, "POST", `/repos/${context.owner}/${context.repo}/labels`, {
|
|
451
|
+
name,
|
|
452
|
+
color,
|
|
453
|
+
description
|
|
454
|
+
});
|
|
455
|
+
else throw error;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Get issue details
|
|
460
|
+
*/
|
|
461
|
+
async function getIssue(context, issueNumber) {
|
|
462
|
+
return await githubRequest(context, "GET", `/repos/${context.owner}/${context.repo}/issues/${issueNumber}`);
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/shared/labels.ts
|
|
466
|
+
/**
|
|
467
|
+
* Standard label set organized by category
|
|
468
|
+
*/
|
|
469
|
+
var STANDARD_LABELS = {
|
|
470
|
+
type: [
|
|
471
|
+
{
|
|
472
|
+
name: "type: bug",
|
|
473
|
+
color: "d73a4a",
|
|
474
|
+
description: "Something isn't working"
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: "type: feature",
|
|
478
|
+
color: "0075ca",
|
|
479
|
+
description: "New feature or enhancement"
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: "type: docs",
|
|
483
|
+
color: "0075ca",
|
|
484
|
+
description: "Documentation improvements"
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: "type: maintenance",
|
|
488
|
+
color: "6c757d",
|
|
489
|
+
description: "Maintenance and refactoring"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: "type: research",
|
|
493
|
+
color: "a371f7",
|
|
494
|
+
description: "Research and investigation"
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: "type: question",
|
|
498
|
+
color: "d876e3",
|
|
499
|
+
description: "Question or discussion"
|
|
500
|
+
}
|
|
501
|
+
],
|
|
502
|
+
priority: [
|
|
503
|
+
{
|
|
504
|
+
name: "priority: critical",
|
|
505
|
+
color: "b60205",
|
|
506
|
+
description: "Critical priority, needs immediate attention"
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
name: "priority: high",
|
|
510
|
+
color: "d93f0b",
|
|
511
|
+
description: "High priority"
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: "priority: medium",
|
|
515
|
+
color: "fbca04",
|
|
516
|
+
description: "Medium priority (default)"
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
name: "priority: low",
|
|
520
|
+
color: "fef2c0",
|
|
521
|
+
description: "Low priority"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
name: "priority: icebox",
|
|
525
|
+
color: "e1e4e8",
|
|
526
|
+
description: "Future consideration, keep in Backlog"
|
|
527
|
+
}
|
|
528
|
+
],
|
|
529
|
+
size: [
|
|
530
|
+
{
|
|
531
|
+
name: "size: xs",
|
|
532
|
+
color: "c2e0c6",
|
|
533
|
+
description: "Extra small (< 2 hours)"
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: "size: s",
|
|
537
|
+
color: "7bd88f",
|
|
538
|
+
description: "Small (2-4 hours)"
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "size: m",
|
|
542
|
+
color: "3fb950",
|
|
543
|
+
description: "Medium (~1 day)"
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
name: "size: l",
|
|
547
|
+
color: "2ea043",
|
|
548
|
+
description: "Large (2-3 days)"
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
name: "size: xl",
|
|
552
|
+
color: "1a7f37",
|
|
553
|
+
description: "Extra large (> 3 days)"
|
|
554
|
+
}
|
|
555
|
+
],
|
|
556
|
+
status: [
|
|
557
|
+
{
|
|
558
|
+
name: "status: blocked",
|
|
559
|
+
color: "d73a4a",
|
|
560
|
+
description: "Blocked by external dependency"
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: "status: help-wanted",
|
|
564
|
+
color: "008672",
|
|
565
|
+
description: "Community contributions welcome"
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
name: "status: good-first-issue",
|
|
569
|
+
color: "7057ff",
|
|
570
|
+
description: "Good for newcomers"
|
|
571
|
+
}
|
|
572
|
+
]
|
|
573
|
+
};
|
|
574
|
+
/**
|
|
575
|
+
* Area labels are repository-specific, so we provide a template
|
|
576
|
+
*/
|
|
577
|
+
var AREA_LABEL_TEMPLATE = [
|
|
578
|
+
{
|
|
579
|
+
name: "area: core",
|
|
580
|
+
color: "fbca04",
|
|
581
|
+
description: "Core functionality"
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
name: "area: api",
|
|
585
|
+
color: "fbca04",
|
|
586
|
+
description: "API-related"
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
name: "area: ui",
|
|
590
|
+
color: "fbca04",
|
|
591
|
+
description: "User interface"
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: "area: cli",
|
|
595
|
+
color: "fbca04",
|
|
596
|
+
description: "Command-line interface"
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: "area: docs",
|
|
600
|
+
color: "fbca04",
|
|
601
|
+
description: "Documentation"
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
name: "area: infra",
|
|
605
|
+
color: "fbca04",
|
|
606
|
+
description: "Infrastructure and deployment"
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
name: "area: tests",
|
|
610
|
+
color: "fbca04",
|
|
611
|
+
description: "Testing infrastructure"
|
|
612
|
+
}
|
|
613
|
+
];
|
|
614
|
+
/**
|
|
615
|
+
* Get all standard labels as a flat array
|
|
616
|
+
*/
|
|
617
|
+
function getAllStandardLabels() {
|
|
618
|
+
return Object.values(STANDARD_LABELS).flat();
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Get labels by category
|
|
622
|
+
*/
|
|
623
|
+
function getLabelsByCategory(category) {
|
|
624
|
+
return STANDARD_LABELS[category] || [];
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Map old label names to new standard names
|
|
628
|
+
*/
|
|
629
|
+
var LABEL_MIGRATIONS = {
|
|
630
|
+
bug: "type: bug",
|
|
631
|
+
feature: "type: feature",
|
|
632
|
+
enhancement: "type: feature",
|
|
633
|
+
documentation: "type: docs",
|
|
634
|
+
question: "type: question",
|
|
635
|
+
"tech-debt": "type: maintenance",
|
|
636
|
+
epic: "type: feature"
|
|
637
|
+
};
|
|
638
|
+
/**
|
|
639
|
+
* Migrate old label to new standard label
|
|
640
|
+
*/
|
|
641
|
+
function migrateLabel(oldLabel) {
|
|
642
|
+
return LABEL_MIGRATIONS[oldLabel] || oldLabel;
|
|
643
|
+
}
|
|
644
|
+
//#endregion
|
|
645
|
+
//#region src/shared/projects.ts
|
|
646
|
+
/**
|
|
647
|
+
* GitHub Projects V2 API Utilities
|
|
648
|
+
*
|
|
649
|
+
* Handles interaction with GitHub Projects V2 via GraphQL API.
|
|
650
|
+
*/
|
|
651
|
+
/**
|
|
652
|
+
* Add an issue to a project
|
|
653
|
+
*/
|
|
654
|
+
async function addIssueToProject(token, projectId, issueId) {
|
|
655
|
+
return (await githubGraphQL(token, `
|
|
656
|
+
mutation($projectId: ID!, $contentId: ID!) {
|
|
657
|
+
addProjectV2ItemById(input: {
|
|
658
|
+
projectId: $projectId
|
|
659
|
+
contentId: $contentId
|
|
660
|
+
}) {
|
|
661
|
+
item {
|
|
662
|
+
id
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
`, {
|
|
667
|
+
projectId,
|
|
668
|
+
contentId: issueId
|
|
669
|
+
})).addProjectV2ItemById.item.id;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Update project item status
|
|
673
|
+
*/
|
|
674
|
+
async function updateProjectItemStatus(token, projectId, itemId, statusFieldId, statusOptionId) {
|
|
675
|
+
await githubGraphQL(token, `
|
|
676
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) {
|
|
677
|
+
updateProjectV2ItemFieldValue(input: {
|
|
678
|
+
projectId: $projectId
|
|
679
|
+
itemId: $itemId
|
|
680
|
+
fieldId: $fieldId
|
|
681
|
+
value: $value
|
|
682
|
+
}) {
|
|
683
|
+
projectV2Item {
|
|
684
|
+
id
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
`, {
|
|
689
|
+
projectId,
|
|
690
|
+
itemId,
|
|
691
|
+
fieldId: statusFieldId,
|
|
692
|
+
value: { singleSelectOptionId: statusOptionId }
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Get issue's node ID (required for adding to project)
|
|
697
|
+
*/
|
|
698
|
+
async function getIssueNodeId(token, owner, repo, issueNumber) {
|
|
699
|
+
return (await githubGraphQL(token, `
|
|
700
|
+
query($owner: String!, $repo: String!, $issueNumber: Int!) {
|
|
701
|
+
repository(owner: $owner, name: $repo) {
|
|
702
|
+
issue(number: $issueNumber) {
|
|
703
|
+
id
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
`, {
|
|
708
|
+
owner,
|
|
709
|
+
repo,
|
|
710
|
+
issueNumber
|
|
711
|
+
})).repository.issue.id;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Get project item ID for an issue (if it exists in the project)
|
|
715
|
+
*/
|
|
716
|
+
async function getProjectItemId(token, projectId, issueId) {
|
|
717
|
+
const item = (await githubGraphQL(token, `
|
|
718
|
+
query($projectId: ID!) {
|
|
719
|
+
node(id: $projectId) {
|
|
720
|
+
... on ProjectV2 {
|
|
721
|
+
items(first: 100) {
|
|
722
|
+
nodes {
|
|
723
|
+
id
|
|
724
|
+
content {
|
|
725
|
+
... on Issue {
|
|
726
|
+
id
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
`, { projectId })).node.items.nodes.find((node) => node.content.id === issueId);
|
|
735
|
+
return item ? item.id : null;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Move issue to a specific status in the project
|
|
739
|
+
*/
|
|
740
|
+
async function moveIssueToStatus(token, owner, repo, issueNumber, config, statusName) {
|
|
741
|
+
const statusOptionId = config.statusOptions[statusName];
|
|
742
|
+
if (!statusOptionId) throw new Error(`Status "${statusName}" not found in project configuration. Available: ${Object.keys(config.statusOptions).join(", ")}`);
|
|
743
|
+
console.log(`Moving issue #${issueNumber} to status "${statusName}"...`);
|
|
744
|
+
const issueId = await getIssueNodeId(token, owner, repo, issueNumber);
|
|
745
|
+
let itemId = await getProjectItemId(token, config.projectId, issueId);
|
|
746
|
+
if (!itemId) {
|
|
747
|
+
console.log("Issue not in project, adding...");
|
|
748
|
+
itemId = await addIssueToProject(token, config.projectId, issueId);
|
|
749
|
+
}
|
|
750
|
+
console.log(`Updating status to "${statusName}"...`);
|
|
751
|
+
await updateProjectItemStatus(token, config.projectId, itemId, config.statusFieldId, statusOptionId);
|
|
752
|
+
console.log(`✅ Issue moved to "${statusName}"`);
|
|
753
|
+
}
|
|
754
|
+
//#endregion
|
|
755
|
+
//#region src/triage/analyze.ts
|
|
756
|
+
/**
|
|
757
|
+
* GitHub Models AI Analysis
|
|
758
|
+
*/
|
|
759
|
+
var VALID_TYPES = [
|
|
760
|
+
"bug",
|
|
761
|
+
"feature",
|
|
762
|
+
"docs",
|
|
763
|
+
"maintenance",
|
|
764
|
+
"research",
|
|
765
|
+
"question"
|
|
766
|
+
];
|
|
767
|
+
var VALID_PRIORITIES = [
|
|
768
|
+
"critical",
|
|
769
|
+
"high",
|
|
770
|
+
"medium",
|
|
771
|
+
"low",
|
|
772
|
+
"icebox"
|
|
773
|
+
];
|
|
774
|
+
var VALID_SIZES = [
|
|
775
|
+
"xs",
|
|
776
|
+
"s",
|
|
777
|
+
"m",
|
|
778
|
+
"l",
|
|
779
|
+
"xl"
|
|
780
|
+
];
|
|
781
|
+
function validateAIAnalysis(parsed) {
|
|
782
|
+
if (!parsed || typeof parsed !== "object") throw new Error("AI response is not a valid object");
|
|
783
|
+
const response = parsed;
|
|
784
|
+
if (!response.type || typeof response.type !== "string") throw new Error("AI response missing required field: type");
|
|
785
|
+
if (!response.priority || typeof response.priority !== "string") throw new Error("AI response missing required field: priority");
|
|
786
|
+
if (!response.size || typeof response.size !== "string") throw new Error("AI response missing required field: size");
|
|
787
|
+
if (!response.reasoning || typeof response.reasoning !== "string") throw new Error("AI response missing required field: reasoning");
|
|
788
|
+
if (!VALID_TYPES.includes(response.type)) throw new Error(`Invalid type: ${response.type}`);
|
|
789
|
+
if (!VALID_PRIORITIES.includes(response.priority)) throw new Error(`Invalid priority: ${response.priority}`);
|
|
790
|
+
if (!VALID_SIZES.includes(response.size)) throw new Error(`Invalid size: ${response.size}`);
|
|
791
|
+
if (response.affected_packages !== void 0) {
|
|
792
|
+
if (!Array.isArray(response.affected_packages)) throw new Error("affected_packages must be an array");
|
|
793
|
+
if (!response.affected_packages.every((pkg) => typeof pkg === "string")) throw new Error("affected_packages must contain only strings");
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
type: response.type,
|
|
797
|
+
priority: response.priority,
|
|
798
|
+
size: response.size,
|
|
799
|
+
reasoning: response.reasoning,
|
|
800
|
+
...response.affected_packages && { affected_packages: response.affected_packages }
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Analyze a GitHub issue using AI to determine type, priority, size, and affected packages.
|
|
805
|
+
*
|
|
806
|
+
* Sends the issue details to the configured AI provider and validates the structured response.
|
|
807
|
+
*
|
|
808
|
+
* @param context - Triage context with issue details and repository config
|
|
809
|
+
* @returns Validated AI analysis with type, priority, size, and reasoning
|
|
810
|
+
* @throws If the AI response cannot be parsed or fails validation
|
|
811
|
+
*/
|
|
812
|
+
async function analyzeIssue(context) {
|
|
813
|
+
return validateAIAnalysis(parseAIJson(await getAICompletion([{
|
|
814
|
+
role: "system",
|
|
815
|
+
content: "You are an expert GitHub issue triager. Analyze issues and provide structured triage information in JSON format."
|
|
816
|
+
}, {
|
|
817
|
+
role: "user",
|
|
818
|
+
content: buildAnalysisPrompt(context)
|
|
819
|
+
}])));
|
|
820
|
+
}
|
|
821
|
+
function buildAnalysisPrompt(context) {
|
|
822
|
+
const { config, issueNumber, issueTitle, issueBody, issueAuthor } = context;
|
|
823
|
+
let prompt = `Analyze this GitHub issue and provide triage information for our kanban workflow.
|
|
824
|
+
|
|
825
|
+
Repository: ${context.owner}/${context.repo} (${config.repoDescription})
|
|
826
|
+
|
|
827
|
+
Issue #${issueNumber}
|
|
828
|
+
Title: ${issueTitle}
|
|
829
|
+
Body: ${issueBody || "(empty)"}
|
|
830
|
+
Author: ${issueAuthor}
|
|
831
|
+
|
|
832
|
+
Determine:
|
|
833
|
+
1. **type**: One of: bug, feature, docs, maintenance, research, question
|
|
834
|
+
2. **priority**: critical, high, medium, low, icebox
|
|
835
|
+
- Use "icebox" for low priority or future consideration items
|
|
836
|
+
3. **size**: Estimated effort: xs (<2hr), s (2-4hr), m (~1day), l (2-3days), xl (>3days)`;
|
|
837
|
+
if (config.packagePattern) {
|
|
838
|
+
prompt += `
|
|
839
|
+
4. **affected_packages**: List of ${config.packagePattern} packages affected`;
|
|
840
|
+
if (config.packageExamples && config.packageExamples.length > 0) prompt += ` (e.g., ${JSON.stringify(config.packageExamples)})`;
|
|
841
|
+
prompt += `
|
|
842
|
+
5. **reasoning**: Brief explanation of your analysis (2-3 sentences)`;
|
|
843
|
+
} else prompt += `
|
|
844
|
+
4. **reasoning**: Brief explanation of your analysis (2-3 sentences)`;
|
|
845
|
+
prompt += `
|
|
846
|
+
|
|
847
|
+
Return JSON in this exact format:
|
|
848
|
+
{
|
|
849
|
+
"type": "bug|feature|docs|maintenance|research|question",
|
|
850
|
+
"priority": "critical|high|medium|low|icebox",
|
|
851
|
+
"size": "xs|s|m|l|xl",`;
|
|
852
|
+
if (config.packagePattern) prompt += `
|
|
853
|
+
"affected_packages": ["package names"],`;
|
|
854
|
+
prompt += `
|
|
855
|
+
"reasoning": "explanation here"
|
|
856
|
+
}`;
|
|
857
|
+
return prompt;
|
|
858
|
+
}
|
|
859
|
+
//#endregion
|
|
860
|
+
//#region src/triage/github.ts
|
|
861
|
+
/**
|
|
862
|
+
* GitHub API Helper Functions
|
|
863
|
+
*/
|
|
864
|
+
async function githubAPI(token, method, path, data = null) {
|
|
865
|
+
return new Promise((resolve, reject) => {
|
|
866
|
+
const options = {
|
|
867
|
+
hostname: "api.github.com",
|
|
868
|
+
path,
|
|
869
|
+
method,
|
|
870
|
+
headers: {
|
|
871
|
+
Authorization: `token ${token}`,
|
|
872
|
+
Accept: "application/vnd.github+json",
|
|
873
|
+
"User-Agent": "happyvertical-github-actions",
|
|
874
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
if (data) options.headers = {
|
|
878
|
+
...options.headers,
|
|
879
|
+
"Content-Type": "application/json"
|
|
880
|
+
};
|
|
881
|
+
const req = https.request(options, (res) => {
|
|
882
|
+
let body = "";
|
|
883
|
+
res.on("data", (chunk) => body += chunk);
|
|
884
|
+
res.on("end", () => {
|
|
885
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve(body ? JSON.parse(body) : null);
|
|
886
|
+
else reject(/* @__PURE__ */ new Error(`GitHub API error: ${res.statusCode} ${body}`));
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
req.on("error", reject);
|
|
890
|
+
if (data) req.write(JSON.stringify(data));
|
|
891
|
+
req.end();
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
//#endregion
|
|
895
|
+
//#region src/triage/comment.ts
|
|
896
|
+
/**
|
|
897
|
+
* Issue Comment Posting
|
|
898
|
+
*/
|
|
899
|
+
/**
|
|
900
|
+
* Post a structured triage comment on the issue with AI analysis results and duplicate links.
|
|
901
|
+
*
|
|
902
|
+
* @param context - Triage context with repo info and issue number
|
|
903
|
+
* @param analysis - AI analysis results (type, priority, size, reasoning)
|
|
904
|
+
* @param duplicates - Array of potential duplicate issues to include in the comment
|
|
905
|
+
*/
|
|
906
|
+
async function postTriageComment(context, analysis, duplicates) {
|
|
907
|
+
const comment = buildTriageComment(context, analysis, duplicates);
|
|
908
|
+
const path = `/repos/${context.owner}/${context.repo}/issues/${context.issueNumber}/comments`;
|
|
909
|
+
try {
|
|
910
|
+
await githubAPI(context.token, "POST", path, { body: comment });
|
|
911
|
+
console.log("Posted triage comment");
|
|
912
|
+
} catch (error) {
|
|
913
|
+
console.error("Error posting comment:", error.message);
|
|
914
|
+
throw error;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Post a comment indicating that automated triage failed, prompting manual triage.
|
|
919
|
+
*
|
|
920
|
+
* @param context - Triage context with repo info and issue number
|
|
921
|
+
* @param error - The error that caused triage to fail
|
|
922
|
+
*/
|
|
923
|
+
async function postErrorComment(context, error) {
|
|
924
|
+
const comment = `## 🤖 AI Triage Failed\n\nAutomated triage encountered an error:\n\`\`\`\n${error.message}\n\`\`\`\n\nPlease triage this issue manually.`;
|
|
925
|
+
const path = `/repos/${context.owner}/${context.repo}/issues/${context.issueNumber}/comments`;
|
|
926
|
+
try {
|
|
927
|
+
await githubAPI(context.token, "POST", path, { body: comment });
|
|
928
|
+
} catch (err) {
|
|
929
|
+
console.error("Error posting error comment:", err.message);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
function buildTriageComment(context, analysis, duplicates) {
|
|
933
|
+
let comment = `## 🤖 AI Triage\n\n`;
|
|
934
|
+
comment += `**Type**: \`${analysis.type}\`\n`;
|
|
935
|
+
comment += `**Priority**: \`${analysis.priority}\`\n`;
|
|
936
|
+
comment += `**Size**: \`${analysis.size}\`\n\n`;
|
|
937
|
+
if (analysis.affected_packages && analysis.affected_packages.length > 0) {
|
|
938
|
+
const packages = analysis.affected_packages.map((p) => `\`${p}\``).join(", ");
|
|
939
|
+
comment += `**Affected Packages**: ${packages}\n\n`;
|
|
940
|
+
}
|
|
941
|
+
comment += `**Analysis**: ${analysis.reasoning}\n\n`;
|
|
942
|
+
if (duplicates.length > 0) {
|
|
943
|
+
comment += `### ⚠️ Potential Duplicates\n\n`;
|
|
944
|
+
duplicates.forEach((dup) => {
|
|
945
|
+
comment += `- #${dup.number}: ${dup.title}\n`;
|
|
946
|
+
});
|
|
947
|
+
comment += `\n`;
|
|
948
|
+
}
|
|
949
|
+
comment += `---\n`;
|
|
950
|
+
comment += `*This triage was performed automatically using GitHub Models API. Please review and adjust labels as needed.*`;
|
|
951
|
+
return comment;
|
|
952
|
+
}
|
|
953
|
+
//#endregion
|
|
954
|
+
//#region src/triage/duplicates.ts
|
|
955
|
+
/**
|
|
956
|
+
* Duplicate Issue Detection
|
|
957
|
+
*/
|
|
958
|
+
/**
|
|
959
|
+
* Search for potential duplicate issues using GitHub Search API.
|
|
960
|
+
*
|
|
961
|
+
* Extracts keywords from the issue title and searches the repository for similar issues.
|
|
962
|
+
* The current issue is excluded from results.
|
|
963
|
+
*
|
|
964
|
+
* @param context - Triage context with issue details and repo info
|
|
965
|
+
* @returns Array of potentially duplicate issues (up to 4)
|
|
966
|
+
*/
|
|
967
|
+
async function searchDuplicates(context) {
|
|
968
|
+
const keywords = context.issueTitle.split(" ").slice(0, 5).join(" ");
|
|
969
|
+
const query = `repo:${context.owner}/${context.repo} is:issue ${keywords}`;
|
|
970
|
+
const path = `/search/issues?q=${encodeURIComponent(query)}&per_page=5`;
|
|
971
|
+
try {
|
|
972
|
+
return (await githubAPI(context.token, "GET", path, null)).items.filter((issue) => issue.number !== context.issueNumber);
|
|
973
|
+
} catch (error) {
|
|
974
|
+
console.error("Error searching for duplicates:", error.message);
|
|
975
|
+
return [];
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
//#endregion
|
|
979
|
+
//#region src/triage/label.ts
|
|
980
|
+
/**
|
|
981
|
+
* Issue Labeling
|
|
982
|
+
*/
|
|
983
|
+
/**
|
|
984
|
+
* Apply labels to an issue using the raw GitHub API.
|
|
985
|
+
* @deprecated Use the v2 export which uses `@happyvertical/repos`.
|
|
986
|
+
*/
|
|
987
|
+
async function applyLabels$1(context, labels) {
|
|
988
|
+
if (labels.length === 0) {
|
|
989
|
+
console.log("No labels to apply");
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
try {
|
|
993
|
+
await addLabels(context, context.issueNumber, labels);
|
|
994
|
+
console.log(`Applied labels: ${labels.join(", ")}`);
|
|
995
|
+
} catch (error) {
|
|
996
|
+
console.error("Error applying labels:", error.message);
|
|
997
|
+
throw error;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Remove an `agent: <type>` label from an issue. Silently ignores 404 errors.
|
|
1002
|
+
* @deprecated Use the v2 export which uses `@happyvertical/repos`.
|
|
1003
|
+
*/
|
|
1004
|
+
async function removeAgentLabel$1(context, agentType) {
|
|
1005
|
+
const label = `agent: ${agentType}`;
|
|
1006
|
+
try {
|
|
1007
|
+
await removeLabel(context, context.issueNumber, label);
|
|
1008
|
+
console.log(`Removed label: ${label}`);
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
if (!error.message.includes("404")) console.error("Error removing label:", error.message);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function getTypeLabel$1(type) {
|
|
1014
|
+
return `type: ${type}`;
|
|
1015
|
+
}
|
|
1016
|
+
function getPriorityLabel$1(priority) {
|
|
1017
|
+
return `priority: ${priority}`;
|
|
1018
|
+
}
|
|
1019
|
+
function getSizeLabel$1(size) {
|
|
1020
|
+
return `size: ${size}`;
|
|
1021
|
+
}
|
|
1022
|
+
function getAgentLabel$1(agentType) {
|
|
1023
|
+
return `agent: ${agentType}`;
|
|
1024
|
+
}
|
|
1025
|
+
//#endregion
|
|
1026
|
+
//#region src/triage/project.ts
|
|
1027
|
+
/**
|
|
1028
|
+
* GitHub Project Board Management
|
|
1029
|
+
*/
|
|
1030
|
+
/**
|
|
1031
|
+
* Update an issue's status on the GitHub Projects V2 board via raw GraphQL.
|
|
1032
|
+
* @deprecated Use the v2 export which uses `@happyvertical/projects`.
|
|
1033
|
+
*/
|
|
1034
|
+
async function updateProjectStatus$1(context, statusName) {
|
|
1035
|
+
if (!context.config.projectEnabled) {
|
|
1036
|
+
console.log("Project board integration disabled");
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (!context.config.projectId || !context.config.statusFieldId) {
|
|
1040
|
+
console.log("Project configuration missing");
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (!context.config.statusOptions || !context.config.statusOptions[statusName]) {
|
|
1044
|
+
console.log(`Status "${statusName}" not configured`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const query = `
|
|
1048
|
+
query($owner: String!, $repo: String!, $number: Int!) {
|
|
1049
|
+
repository(owner: $owner, name: $repo) {
|
|
1050
|
+
issue(number: $number) {
|
|
1051
|
+
projectItems(first: 10) {
|
|
1052
|
+
nodes {
|
|
1053
|
+
id
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
`;
|
|
1060
|
+
try {
|
|
1061
|
+
const variables = {
|
|
1062
|
+
owner: context.owner,
|
|
1063
|
+
repo: context.repo,
|
|
1064
|
+
number: context.issueNumber
|
|
1065
|
+
};
|
|
1066
|
+
const itemId = (await githubAPI(context.token, "POST", "/graphql", {
|
|
1067
|
+
query,
|
|
1068
|
+
variables
|
|
1069
|
+
})).data.repository.issue.projectItems.nodes[0]?.id;
|
|
1070
|
+
if (!itemId) {
|
|
1071
|
+
console.log("Issue not in project board, skipping status update");
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const updateQuery = `
|
|
1075
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
1076
|
+
updateProjectV2ItemFieldValue(
|
|
1077
|
+
input: {
|
|
1078
|
+
projectId: $projectId
|
|
1079
|
+
itemId: $itemId
|
|
1080
|
+
fieldId: $fieldId
|
|
1081
|
+
value: { singleSelectOptionId: $optionId }
|
|
1082
|
+
}
|
|
1083
|
+
) {
|
|
1084
|
+
projectV2Item {
|
|
1085
|
+
id
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
`;
|
|
1090
|
+
const updateVars = {
|
|
1091
|
+
projectId: context.config.projectId,
|
|
1092
|
+
itemId,
|
|
1093
|
+
fieldId: context.config.statusFieldId,
|
|
1094
|
+
optionId: context.config.statusOptions[statusName]
|
|
1095
|
+
};
|
|
1096
|
+
await githubAPI(context.token, "POST", "/graphql", {
|
|
1097
|
+
query: updateQuery,
|
|
1098
|
+
variables: updateVars
|
|
1099
|
+
});
|
|
1100
|
+
console.log(`Updated project status to: ${statusName}`);
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
console.error("Error updating project status:", error.message);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
//#endregion
|
|
1106
|
+
//#region src/triage/index.ts
|
|
1107
|
+
/**
|
|
1108
|
+
* Issue Triage Orchestration
|
|
1109
|
+
*/
|
|
1110
|
+
var triage_exports = /* @__PURE__ */ __exportAll({
|
|
1111
|
+
analyzeIssue: () => analyzeIssue,
|
|
1112
|
+
applyLabels: () => applyLabels$1,
|
|
1113
|
+
getAgentLabel: () => getAgentLabel$1,
|
|
1114
|
+
getPriorityLabel: () => getPriorityLabel$1,
|
|
1115
|
+
getSizeLabel: () => getSizeLabel$1,
|
|
1116
|
+
getTypeLabel: () => getTypeLabel$1,
|
|
1117
|
+
postErrorComment: () => postErrorComment,
|
|
1118
|
+
postTriageComment: () => postTriageComment,
|
|
1119
|
+
removeAgentLabel: () => removeAgentLabel$1,
|
|
1120
|
+
searchDuplicates: () => searchDuplicates,
|
|
1121
|
+
triageIssue: () => triageIssue$1,
|
|
1122
|
+
updateProjectStatus: () => updateProjectStatus$1
|
|
1123
|
+
});
|
|
1124
|
+
/**
|
|
1125
|
+
* @deprecated Use the default export from `@happyvertical/github-actions` instead.
|
|
1126
|
+
* Legacy triage orchestrator using raw GitHub API calls.
|
|
1127
|
+
*/
|
|
1128
|
+
async function triageIssue$1(context) {
|
|
1129
|
+
console.log(`Triaging issue #${context.issueNumber}: ${context.issueTitle}`);
|
|
1130
|
+
try {
|
|
1131
|
+
await applyLabels$1(context, [getAgentLabel$1("triage")]);
|
|
1132
|
+
console.log("Calling AI for analysis...");
|
|
1133
|
+
const analysis = await analyzeIssue(context);
|
|
1134
|
+
console.log("AI Analysis:", JSON.stringify(analysis, null, 2));
|
|
1135
|
+
console.log("Searching for potential duplicates...");
|
|
1136
|
+
const duplicates = await searchDuplicates(context);
|
|
1137
|
+
console.log(`Found ${duplicates.length} potential duplicates`);
|
|
1138
|
+
await applyLabels$1(context, [
|
|
1139
|
+
getTypeLabel$1(analysis.type),
|
|
1140
|
+
getPriorityLabel$1(analysis.priority),
|
|
1141
|
+
getSizeLabel$1(analysis.size)
|
|
1142
|
+
]);
|
|
1143
|
+
await postTriageComment(context, analysis, duplicates);
|
|
1144
|
+
await removeAgentLabel$1(context, "triage");
|
|
1145
|
+
if (context.config.projectEnabled) {
|
|
1146
|
+
console.log("Moving issue to Backlog...");
|
|
1147
|
+
await updateProjectStatus$1(context, "Backlog");
|
|
1148
|
+
}
|
|
1149
|
+
console.log("✅ Triage complete!");
|
|
1150
|
+
return {
|
|
1151
|
+
success: true,
|
|
1152
|
+
analysis,
|
|
1153
|
+
duplicates
|
|
1154
|
+
};
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
console.error("❌ Triage failed:", error.message);
|
|
1157
|
+
console.error(error.stack);
|
|
1158
|
+
await postErrorComment(context, error);
|
|
1159
|
+
return {
|
|
1160
|
+
success: false,
|
|
1161
|
+
error: error.message
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
//#endregion
|
|
1166
|
+
//#region src/triage/label-v2.ts
|
|
1167
|
+
/**
|
|
1168
|
+
* Issue Labeling (Refactored to use @happyvertical/repos)
|
|
1169
|
+
*/
|
|
1170
|
+
/**
|
|
1171
|
+
* Apply labels to an issue using `@happyvertical/repos`.
|
|
1172
|
+
*
|
|
1173
|
+
* @param context - Triage context with token and repo info
|
|
1174
|
+
* @param labels - Label names to apply
|
|
1175
|
+
*/
|
|
1176
|
+
async function applyLabels(context, labels) {
|
|
1177
|
+
if (labels.length === 0) {
|
|
1178
|
+
console.log("No labels to apply");
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
try {
|
|
1182
|
+
await (await createRepository(context.token, context.owner, context.repo)).addLabels(context.issueNumber, labels);
|
|
1183
|
+
console.log(`Applied labels: ${labels.join(", ")}`);
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
console.error("Error applying labels:", error.message);
|
|
1186
|
+
throw error;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Remove an `agent: <type>` label from an issue. Silently ignores 404 errors.
|
|
1191
|
+
*
|
|
1192
|
+
* @param context - Triage context with token and repo info
|
|
1193
|
+
* @param agentType - Agent type suffix (e.g., "triage", "planning")
|
|
1194
|
+
*/
|
|
1195
|
+
async function removeAgentLabel(context, agentType) {
|
|
1196
|
+
const label = `agent: ${agentType}`;
|
|
1197
|
+
try {
|
|
1198
|
+
await (await createRepository(context.token, context.owner, context.repo)).removeLabel(context.issueNumber, label);
|
|
1199
|
+
console.log(`Removed label: ${label}`);
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
if (!error.message.includes("404")) console.error("Error removing label:", error.message);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
function getTypeLabel(type) {
|
|
1205
|
+
return `type: ${type}`;
|
|
1206
|
+
}
|
|
1207
|
+
function getPriorityLabel(priority) {
|
|
1208
|
+
return `priority: ${priority}`;
|
|
1209
|
+
}
|
|
1210
|
+
function getSizeLabel(size) {
|
|
1211
|
+
return `size: ${size}`;
|
|
1212
|
+
}
|
|
1213
|
+
function getAgentLabel(agentType) {
|
|
1214
|
+
return `agent: ${agentType}`;
|
|
1215
|
+
}
|
|
1216
|
+
//#endregion
|
|
1217
|
+
//#region src/triage/project-v2.ts
|
|
1218
|
+
/**
|
|
1219
|
+
* GitHub Project Board Management (Refactored to use @happyvertical/projects)
|
|
1220
|
+
*/
|
|
1221
|
+
/**
|
|
1222
|
+
* Update an issue's status on the GitHub Projects V2 board using `@happyvertical/projects`.
|
|
1223
|
+
*
|
|
1224
|
+
* Adds the issue to the project if not already present, then sets the status field.
|
|
1225
|
+
* No-ops if project integration is disabled or the status name is not configured.
|
|
1226
|
+
*
|
|
1227
|
+
* @param context - Triage context with project config
|
|
1228
|
+
* @param statusName - Target status name (must exist in `config.statusOptions`)
|
|
1229
|
+
*/
|
|
1230
|
+
async function updateProjectStatus(context, statusName) {
|
|
1231
|
+
if (!context.config.projectEnabled) {
|
|
1232
|
+
console.log("Project board integration disabled");
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (!context.config.projectId || !context.config.statusFieldId) {
|
|
1236
|
+
console.log("Project configuration missing");
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (!context.config.statusOptions || !context.config.statusOptions[statusName]) {
|
|
1240
|
+
console.log(`Status "${statusName}" not configured`);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
try {
|
|
1244
|
+
const repo = await createRepository(context.token, context.owner, context.repo);
|
|
1245
|
+
const project = await createProject(context.token, context.config.projectId, context.config.statusFieldId, context.config.statusOptions);
|
|
1246
|
+
const issue = await repo.getIssue(context.issueNumber);
|
|
1247
|
+
let itemId = (await project.listItems()).find((item) => item.contentId === issue.id)?.id;
|
|
1248
|
+
if (!itemId) {
|
|
1249
|
+
console.log("Issue not in project, adding...");
|
|
1250
|
+
itemId = (await project.addItem(issue.id)).id;
|
|
1251
|
+
}
|
|
1252
|
+
await project.updateItemStatus(itemId, statusName);
|
|
1253
|
+
console.log(`Updated project status to: ${statusName}`);
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
console.error("Error updating project status:", error.message);
|
|
1256
|
+
throw error;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
//#endregion
|
|
1260
|
+
//#region src/triage/index-v2.ts
|
|
1261
|
+
/**
|
|
1262
|
+
* Issue Triage Orchestration (Refactored to use @happyvertical/repos and @happyvertical/projects)
|
|
1263
|
+
*/
|
|
1264
|
+
/**
|
|
1265
|
+
* Run the full issue triage workflow using `@happyvertical/repos`.
|
|
1266
|
+
*
|
|
1267
|
+
* Applies an agent label, runs AI analysis, searches for duplicates,
|
|
1268
|
+
* applies type/priority/size labels, posts a triage comment, and
|
|
1269
|
+
* optionally updates the project board status.
|
|
1270
|
+
*
|
|
1271
|
+
* @param context - Triage context including token, repo info, issue details, and config
|
|
1272
|
+
* @returns Result indicating success/failure with analysis and duplicate data
|
|
1273
|
+
*/
|
|
1274
|
+
async function triageIssue(context) {
|
|
1275
|
+
console.log(`Triaging issue #${context.issueNumber}: ${context.issueTitle}`);
|
|
1276
|
+
try {
|
|
1277
|
+
await applyLabels(context, [getAgentLabel("triage")]);
|
|
1278
|
+
console.log("Calling AI for analysis...");
|
|
1279
|
+
const analysis = await analyzeIssue(context);
|
|
1280
|
+
console.log("AI Analysis:", JSON.stringify(analysis, null, 2));
|
|
1281
|
+
console.log("Searching for potential duplicates...");
|
|
1282
|
+
const duplicates = await searchDuplicates(context);
|
|
1283
|
+
console.log(`Found ${duplicates.length} potential duplicates`);
|
|
1284
|
+
await applyLabels(context, [
|
|
1285
|
+
getTypeLabel(analysis.type),
|
|
1286
|
+
getPriorityLabel(analysis.priority),
|
|
1287
|
+
getSizeLabel(analysis.size)
|
|
1288
|
+
]);
|
|
1289
|
+
await postTriageComment(context, analysis, duplicates);
|
|
1290
|
+
await removeAgentLabel(context, "triage");
|
|
1291
|
+
if (context.config.projectEnabled) {
|
|
1292
|
+
console.log("Moving issue to Backlog...");
|
|
1293
|
+
await updateProjectStatus(context, "Backlog");
|
|
1294
|
+
}
|
|
1295
|
+
console.log("✅ Triage complete!");
|
|
1296
|
+
return {
|
|
1297
|
+
success: true,
|
|
1298
|
+
analysis,
|
|
1299
|
+
duplicates
|
|
1300
|
+
};
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
console.error("❌ Triage failed:", error.message);
|
|
1303
|
+
console.error(error.stack);
|
|
1304
|
+
await postErrorComment(context, error);
|
|
1305
|
+
return {
|
|
1306
|
+
success: false,
|
|
1307
|
+
error: error.message
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
//#endregion
|
|
1312
|
+
//#region src/index.ts
|
|
1313
|
+
/** @internal */
|
|
1314
|
+
var PACKAGE_VERSION_INITIALIZED = true;
|
|
1315
|
+
//#endregion
|
|
1316
|
+
export { githubGraphQL as A, startPlanning as B, getAllStandardLabels as C, assignIssue as D, addLabels as E, callGitHubModels as F, isReady as G, postPlanningErrorComment as H, callOpenAI as I, createProject as J, validateDefinitionOfReady as K, getAICompletion as L, postComment as M, removeLabel as N, createOrUpdateLabel as O, callAnthropic as P, parseAIJson as R, STANDARD_LABELS as S, migrateLabel as T, postReadyCheckComment as U, postPlanComment as V, formatDefinitionOfReady as W, createRepository as Y, getProjectItemId as _, getAgentLabel as a, AREA_LABEL_TEMPLATE as b, getTypeLabel as c, searchDuplicates as d, postErrorComment as f, getIssueNodeId as g, addIssueToProject as h, applyLabels as i, githubRequest as j, getIssue as k, removeAgentLabel as l, analyzeIssue as m, triageIssue as n, getPriorityLabel as o, postTriageComment as p, analyzePlanning as q, updateProjectStatus as r, getSizeLabel as s, PACKAGE_VERSION_INITIALIZED as t, triage_exports as u, moveIssueToStatus as v, getLabelsByCategory as w, LABEL_MIGRATIONS as x, updateProjectItemStatus as y, completePlanning as z };
|
|
1317
|
+
|
|
1318
|
+
//# sourceMappingURL=src-CezXcNYi.js.map
|