Skip to content

Commit 418f46f

Browse files
committed
Revert "fix: remove AI triage workflow"
This reverts commit a183369.
1 parent a183369 commit 418f46f

1 file changed

Lines changed: 229 additions & 0 deletions

File tree

.github/workflows/ai-triage.yml

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
name: AI Issue Triage
2+
on:
3+
issues:
4+
types:
5+
- opened
6+
- edited
7+
8+
permissions:
9+
issues: write
10+
contents: read
11+
12+
jobs:
13+
ai-triage:
14+
if: github.repository_owner == 'GameServerManagers'
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Triage issue with GitHub Models
18+
uses: actions/github-script@v7
19+
env:
20+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21+
with:
22+
script: |
23+
const title = context.payload.issue.title || '';
24+
const body = context.payload.issue.body || '';
25+
const number = context.payload.issue.number;
26+
const owner = context.repo.owner;
27+
const repo = context.repo.repo;
28+
const AI_MARKER = '<!-- ai-triage -->';
29+
30+
function parseTriageResponse(raw) {
31+
const input = (raw || '').trim();
32+
if (!input) return {};
33+
34+
const candidates = [input];
35+
const fenced = input.match(/```(?:json)?\s*([\s\S]*?)```/i);
36+
if (fenced?.[1]) candidates.push(fenced[1].trim());
37+
38+
const firstBrace = input.indexOf('{');
39+
const lastBrace = input.lastIndexOf('}');
40+
if (firstBrace !== -1 && lastBrace > firstBrace) {
41+
candidates.push(input.slice(firstBrace, lastBrace + 1));
42+
}
43+
44+
for (const candidate of candidates) {
45+
try {
46+
return JSON.parse(candidate);
47+
} catch (_err) {
48+
// Continue trying fallbacks.
49+
}
50+
}
51+
52+
return {};
53+
}
54+
55+
// For short bodies, apply "needs: more info" label directly.
56+
// Skip the AI call but still label the issue.
57+
const isShortBody = body.trim().length < 80;
58+
if (isShortBody) {
59+
try {
60+
await github.rest.issues.addLabels({
61+
owner, repo, issue_number: number,
62+
labels: ['needs: more info'],
63+
});
64+
} catch (err) {
65+
console.log('Could not apply label for short body:', err.message);
66+
}
67+
return;
68+
}
69+
70+
// ── Call GitHub Models ────────────────────────────────────────
71+
let triage;
72+
try {
73+
const res = await fetch(
74+
'https://models.inference.ai.azure.com/chat/completions',
75+
{
76+
method: 'POST',
77+
headers: {
78+
'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`,
79+
'Content-Type': 'application/json',
80+
},
81+
body: JSON.stringify({
82+
model: 'gpt-4o-mini',
83+
temperature: 0.1,
84+
max_tokens: 400,
85+
messages: [
86+
{
87+
role: 'system',
88+
content:
89+
'You are a triage assistant for LinuxGSM, an open-source ' +
90+
'Linux game server manager. Your role is to:\n' +
91+
'1. Analyze issue quality (completeness, clarity)\n' +
92+
'2. Extract game names mentioned in the issue, even if misspelled or abbreviated\n' +
93+
'3. Suggest corrections for likely typos using fuzzy matching\n' +
94+
'4. Respond ONLY with a valid JSON object — no markdown fences.\n\n' +
95+
'Common game name variations and typos you should recognize:\n' +
96+
'- "Valhiem" → "Valheim"\n' +
97+
'- "Rrust" → "Rust"\n' +
98+
'- "Conterstrike" / "CS" / "CSGO" → "Counter-Strike: Global Offensive"\n' +
99+
'- "Garrys" / "GMod" → "Garrys Mod"\n' +
100+
'- "ARK" / "Ark" → "ARK: Survival Evolved"\n' +
101+
'- "DayZ" / "Dayz" → "DayZ"\n' +
102+
'- "Insurgency Sandstorm" / "Insurgency 2" → "Insurgency: Sandstorm"',
103+
},
104+
{
105+
role: 'user',
106+
content:
107+
`Title: ${title}\n\nBody:\n${body.slice(0, 3000)}\n\n` +
108+
'Respond with this JSON schema:\n' +
109+
'{\n' +
110+
' "quality": "good" | "ok" | "poor",\n' +
111+
' "missing_info": ["list of specific missing fields"],\n' +
112+
' "detected_game": "canonical game name if one is mentioned, or null",\n' +
113+
' "game_confidence": "high" | "medium" | "low" | null,\n' +
114+
' "game_note": "correction suggestion if the user misspelled a game name, or empty string",\n' +
115+
' "comment": "one or two sentence note to the reporter, or empty string"\n' +
116+
'}',
117+
},
118+
],
119+
}),
120+
}
121+
);
122+
123+
if (!res.ok) {
124+
console.log(`GitHub Models returned ${res.status} — skipping AI triage.`);
125+
return;
126+
}
127+
128+
const data = await res.json();
129+
const raw = data.choices?.[0]?.message?.content || '{}';
130+
triage = parseTriageResponse(raw);
131+
} catch (err) {
132+
// Never fail the workflow if the AI call errors — it's advisory only.
133+
console.log('AI triage skipped:', err.message);
134+
return;
135+
}
136+
137+
if (!triage || typeof triage !== 'object') {
138+
triage = {};
139+
}
140+
141+
// ── Act on the result ────────────────────────────────────────
142+
const isPoor = triage.quality === 'poor';
143+
const missing = Array.isArray(triage.missing_info) ? triage.missing_info : [];
144+
const hasIssues = isPoor || missing.length > 0;
145+
146+
// Prepare labels to apply
147+
const labelsToApply = [];
148+
149+
// Check if a game was detected with high confidence
150+
const detectedGame = triage.detected_game;
151+
const gameConfidence = triage.game_confidence;
152+
153+
if (detectedGame && gameConfidence === 'high') {
154+
labelsToApply.push(`game: ${detectedGame}`);
155+
}
156+
157+
// Apply "needs: more info" label if quality issues detected
158+
if (hasIssues) {
159+
labelsToApply.push('needs: more info');
160+
}
161+
162+
// Apply labels one-by-one so a single failure does not block all labels.
163+
const uniqueLabels = [...new Set(labelsToApply)];
164+
for (const label of uniqueLabels) {
165+
try {
166+
await github.rest.issues.addLabels({
167+
owner,
168+
repo,
169+
issue_number: number,
170+
labels: [label],
171+
});
172+
} catch (err) {
173+
console.log(`Could not apply label "${label}":`, err.message);
174+
}
175+
}
176+
177+
// Post a comment only when there is something specific to say
178+
const gameNote = triage.game_note || '';
179+
const reporterComment = triage.comment || '';
180+
181+
if (!hasIssues && !gameNote) return;
182+
183+
const missingBlock = missing.length > 0
184+
? `\n\n**Missing information:**\n${missing.map(m => `- ${m}`).join('\n')}`
185+
: '';
186+
187+
const gameBlock = gameNote
188+
? `\n\n**Game name note:** ${gameNote}`
189+
: '';
190+
191+
const triageCommentBody =
192+
`${AI_MARKER}\n` +
193+
`Thanks for opening this issue! 👋\n\n` +
194+
`${reporterComment}` +
195+
`${missingBlock}` +
196+
`${gameBlock}\n\n` +
197+
`_This note was generated automatically by AI triage and may not be perfect. ` +
198+
`A maintainer will review shortly._`;
199+
200+
try {
201+
const comments = await github.rest.issues.listComments({
202+
owner,
203+
repo,
204+
issue_number: number,
205+
per_page: 100,
206+
});
207+
208+
const existingAiComment = comments.data.find(
209+
(comment) => comment.user?.type === 'Bot' && comment.body?.includes(AI_MARKER)
210+
);
211+
212+
if (existingAiComment) {
213+
await github.rest.issues.updateComment({
214+
owner,
215+
repo,
216+
comment_id: existingAiComment.id,
217+
body: triageCommentBody,
218+
});
219+
} else {
220+
await github.rest.issues.createComment({
221+
owner,
222+
repo,
223+
issue_number: number,
224+
body: triageCommentBody,
225+
});
226+
}
227+
} catch (err) {
228+
console.log('Could not post comment:', err.message);
229+
}

0 commit comments

Comments
 (0)