From 8add830d2b1b2229a030332ab2f3b85022df10b7 Mon Sep 17 00:00:00 2001 From: ztimson Date: Tue, 30 Dec 2025 23:17:45 -0500 Subject: [PATCH] Check for duplicates before adding tickets --- src/refine.mjs | 78 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/src/refine.mjs b/src/refine.mjs index 33759f4..5c19dc7 100644 --- a/src/refine.mjs +++ b/src/refine.mjs @@ -28,14 +28,14 @@ dotenv.config({path: '.env.local', override: true, quiet: true}); // Fetch issue const issueRes = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, { headers: {'Authorization': `token ${auth}`} - }); - if(!issueRes.ok) throw new Error(`${issueRes.status} ${await issueRes.text()}`); + }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); const issueData = await issueRes.json(); - if(!issueData.labels?.some(l => l.name === 'Review/AI')) { + if(issueData.labels[0] !== 'Review/AI' || issueData.labels.length !== 1) { console.log('Skipping'); return process.exit(); } + // Gather readme & template let title = '', type = '', readme = '', readmeP = path.join(process.cwd(), 'README.md'); if(fs.existsSync(readmeP)) readme = fs.readFileSync(readmeP, 'utf-8'); const template = p ? fs.readFileSync(p, 'utf-8') : `## Description @@ -80,6 +80,7 @@ Implementation details, constraints, dependencies, design decisions | **Total** | **0-15** | `; + // Create AI let options = {ollama: {model, host}}; if(host === 'anthropic') options = {anthropic: {model, token}}; else if(host === 'openai') options = {openAi: {model, token}}; @@ -141,34 +142,61 @@ ${template.trim()} Output ONLY markdown. No explanations, labels, or extra formatting.`}); + // Format ticket with AI const messages = await ai.language.ask(`Title: ${issueData.title}\n\nDescription:\n${issueData.body || 'No description provided'}`).catch(() => []); const body = messages?.pop()?.content; - if(!body) { - console.log('Invalid response from AI'); - return process.exit(1); + if(!body) throw new Error('Invalid response from AI'); + + // Check for duplicates + const search = await fetch(`${git}/api/v1/repos/issues/search`, { + method: 'POST', + headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, + body: JSON.stringify({ + priority_repo_id: repo, + type: 'issues', + limit: 3, + q: title + }) + }).then(resp => resp.ok ? resp.json() : []); + let dupeId = null; + const dupeIds = search.map(t => t.id); + const dupes = search.map(t => `ID: ${t.id}\nTitle: ${t.title}\n\`\`\`markdown\n${t.body}\n\`\`\``).join('\n\n'); + const hasDuplicates = (await ai.language.ask(`${title}\n\`\`\`markdown\n${body}\n\`\`\``, { + system: `Your job is to identify duplicates. Respond with the ID number of the duplicate or nothing if there are no matches \n\n${dupes}` + }))?.pop()?.content; + // Handle duplicates + if(!!hasDuplicates && (dupeId = dupeIds.find(id => hasDuplicates.includes(id.toString()))) != null) { + await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/comments`, { + method: 'POST', + headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, + body: `{"body": "Duplicate of #${dupeId}"}` + }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); + await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, { + method: 'POST', + headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, + body: '{"labels":["Reviewed/Duplicate"]}' + }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); + await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, { + method: 'PATCH', + headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, + body: '{"state": "closed"}' + }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); + console.log('Duplicate'); + return process.exit(); } + + // Update ticket const updateRes = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, { method: 'PATCH', - headers: { - 'Authorization': `token ${auth}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - title, - body, - }) - }); - if(!updateRes.ok) throw new Error(`${updateRes.status} ${await updateRes.text()}`); - if(type) { - const resp = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, { + headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, + body: JSON.stringify({title, body}) + }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); + if(type) { // Label + await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, { method: 'POST', - headers: { - 'Authorization': `token ${auth}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({labels: [`Kind/${type[0].toUpperCase() + type.slice(1).toLowerCase()}`]}) - }); - if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); + headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, + body: `{"labels":["Reviewed/${type[0].toUpperCase() + type.slice(1).toLowerCase()}"]}` + }).then(async resp => !resp.ok ? throw new Error(`${updateRes.status} ${await updateRes.text()}`) : null); } console.log(`Title: ${title}\nType: ${type}\nBody:\n${body}`);