1name: Identify potential duplicates among new bug/crash reports
2
3on:
4 issues:
5 types: [opened]
6 workflow_dispatch:
7 inputs:
8 issue_number:
9 description: "Issue number to analyze (for testing)"
10 required: true
11 type: number
12
13concurrency:
14 group: potential-duplicate-check-${{ github.event.issue.number || inputs.issue_number }}
15 cancel-in-progress: true
16
17jobs:
18 identify-duplicates:
19 # For manual testing, allow running on any branch; for automatic runs, only on main repo
20 if: github.event_name == 'workflow_dispatch' || github.repository == 'zed-industries/zed'
21 runs-on: ubuntu-latest
22 timeout-minutes: 5
23
24 permissions:
25 contents: read
26 issues: read
27
28 steps:
29 - name: Get github app token
30 id: get-app-token
31 uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1 # v2.1.4
32 with:
33 app-id: ${{ secrets.ZED_COMMUNITY_BOT_APP_ID }}
34 private-key: ${{ secrets.ZED_COMMUNITY_BOT_PRIVATE_KEY }}
35 owner: zed-industries
36
37 - name: Fetch issue and check eligibility
38 id: fetch-issue
39 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
40 with:
41 github-token: ${{ steps.get-app-token.outputs.token }}
42 script: |
43 const issueNumber = context.payload.issue?.number || ${{ inputs.issue_number || 0 }};
44 if (!issueNumber) {
45 core.setFailed('No issue number provided');
46 return;
47 }
48
49 const { data: issue } = await github.rest.issues.get({
50 owner: context.repo.owner,
51 repo: context.repo.repo,
52 issue_number: issueNumber
53 });
54
55 const typeName = issue.type?.name;
56 const isTargetType = typeName === 'Bug' || typeName === 'Crash';
57
58 console.log(`Issue #${issueNumber}: "${issue.title}"`);
59 console.log(`Issue type: ${typeName || '(none)'}`);
60 console.log(`Is target type (Bug/Crash): ${isTargetType}`);
61
62 // Set default outputs for all paths
63 core.setOutput('issue_number', issueNumber);
64 core.setOutput('issue_title', issue.title);
65 core.setOutput('issue_body', (issue.body || '').slice(0, 6000));
66 core.setOutput('is_target_type', String(isTargetType));
67 core.setOutput('is_staff', 'false');
68 core.setOutput('should_continue', 'false');
69
70 if (!isTargetType) {
71 console.log('::notice::Skipping - issue type is not Bug or Crash');
72 return;
73 }
74
75 // Check if author is staff (skip if so - they know what they're doing)
76 const author = issue.user?.login || '';
77 let isStaff = false;
78 if (author) {
79 try {
80 const response = await github.rest.teams.getMembershipForUserInOrg({
81 org: 'zed-industries',
82 team_slug: 'staff',
83 username: author
84 });
85 isStaff = response.data.state === 'active';
86 } catch (error) {
87 if (error.status !== 404) throw error;
88 }
89 }
90
91 core.setOutput('is_staff', String(isStaff));
92 if (isStaff) {
93 console.log(`::notice::Skipping - author @${author} is a staff member`);
94 return;
95 }
96
97 core.setOutput('should_continue', 'true');
98
99 # ========================================================================
100 # PASS 1: Detect areas using Claude with the full area taxonomy
101 # ========================================================================
102 - name: "Pass 1: Detect areas with Claude"
103 if: steps.fetch-issue.outputs.should_continue == 'true'
104 id: detect-areas
105 env:
106 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_ISSUE_DEDUP }}
107 ISSUE_TITLE: ${{ steps.fetch-issue.outputs.issue_title }}
108 ISSUE_BODY: ${{ steps.fetch-issue.outputs.issue_body }}
109 run: |
110 # shellcheck disable=SC2016
111 cat > /tmp/area_prompt.txt << 'PROMPT_EOF'
112 You are classifying a GitHub issue for the Zed code editor into area categories.
113
114 ## Issue Title
115 ISSUE_TITLE_PLACEHOLDER
116
117 ## Issue Body
118 ISSUE_BODY_PLACEHOLDER
119
120 ## Available Area Labels
121 (descriptions provided only where the label name isn't self-explanatory)
122
123 accessibility
124 ai, ai/acp (Agent Communication Protocol), ai/agent thread, ai/anthropic, ai/assistant, ai/bedrock, ai/codex, ai/copilot, ai/deepseek, ai/edit prediction, ai/gemini, ai/inline assistant, ai/lmstudio, ai/mcp (Model Context Protocol), ai/mistral, ai/ollama, ai/openai, ai/openai compatible, ai/openrouter, ai/qwen, ai/supermaven, ai/text thread, ai/zeta
125 auth
126 autocompletions
127 billing
128 cli
129 code actions
130 code folding
131 collab - real-time collaboration with other Zed users (screen sharing, shared editing). NOT for remote development over SSH.
132 collab/audio, collab/chat
133 command palette
134 controls/ime, controls/keybinds, controls/mouse
135 debugger, debugger/dap/CodeLLDB, debugger/dap/debugpy, debugger/dap/gdb, debugger/dap/javascript
136 design papercut - small UI/UX polish issues
137 dev containers - Docker-based development environments
138 diagnostics - LSP errors/warnings display
139 discoverability
140 editor, editor/brackets, editor/linked edits
141 extensions/infrastructure
142 file finder - fuzzy file search (Cmd/Ctrl+P)
143 gpui - Zed's internal UI rendering framework
144 inlay hints - inline hints from LSP (type annotations, parameter names)
145 installer-updater
146 integrations/environment - shell environment, PATH, env vars
147 integrations/git, integrations/git/blame, integrations/terminal
148 internationalization, internationalization/rtl support
149 keymap editor
150 language server, language server/server failure
151 languages/* - language-specific syntax, grammar, or LSP issues (e.g., languages/python, languages/rust, languages/typescript)
152 legal
153 logging
154 multi-buffer - viewing multiple files or search results in a single editor pane
155 multi-cursor
156 navigation - go to definition, find references, symbol search
157 network - proxy settings, connectivity, SSL certificates. NOT for collab.
158 onboarding
159 outline - document symbols/structure sidebar
160 parity/* - feature parity requests comparing to other editors (parity/vscode, parity/vim, parity/emacs, parity/jetbrains, parity/helix)
161 performance, performance/memory leak
162 permissions
163 popovers - hover cards, tooltips, autocomplete dropdowns
164 preview/images, preview/markdown
165 project panel - file tree sidebar
166 release notes
167 repl
168 search - project-wide search, find/replace
169 security & privacy, security & privacy/workspace trust
170 serialization - saving/restoring workspace state, undo history, folding state across restarts
171 settings, settings/ui
172 snippets
173 status bar
174 tasks - task runner integration
175 telemetry
176 tooling/* - external tool integrations (tooling/emmet, tooling/eslint, tooling/prettier, tooling/flatpak, tooling/nix)
177 tree-sitter - syntax parsing and highlighting engine
178 ui/animations, ui/dock, ui/file icons, ui/font, ui/menus, ui/minimap, ui/panel, ui/scaling, ui/scrolling, ui/tabs, ui/themes
179 workspace - window management, pane layout, project handling
180 zed account
181 zed.dev
182
183 ## Your Task
184
185 Based on the issue title and body, identify which areas this issue relates to.
186 - Select 1-5 areas that best match the issue
187 - Prefer more specific sub-areas when applicable (e.g., "ai/gemini" over just "ai")
188 - Only select areas that are clearly relevant
189
190 ## Response Format
191
192 Return ONLY a JSON object (no markdown fences, no explanation):
193 {
194 "areas": ["area1", "area2"],
195 "reasoning": "Brief explanation of why these areas were selected"
196 }
197 PROMPT_EOF
198
199 # Single quotes are intentional to prevent bash expansion; node reads env vars via process.env
200 # shellcheck disable=SC2016
201 node << 'SCRIPT_EOF'
202 const fs = require('fs');
203 let prompt = fs.readFileSync('/tmp/area_prompt.txt', 'utf8');
204 prompt = prompt.replace('ISSUE_TITLE_PLACEHOLDER', process.env.ISSUE_TITLE || '');
205 prompt = prompt.replace('ISSUE_BODY_PLACEHOLDER', process.env.ISSUE_BODY || '');
206 fs.writeFileSync('/tmp/area_prompt_final.txt', prompt);
207 SCRIPT_EOF
208
209 HTTP_CODE=$(curl -s -w "%{http_code}" -o /tmp/area_response.json -X POST "https://api.anthropic.com/v1/messages" \
210 -H "Content-Type: application/json" \
211 -H "x-api-key: $ANTHROPIC_API_KEY" \
212 -H "anthropic-version: 2023-06-01" \
213 --data-binary @- << EOF
214 {
215 "model": "claude-sonnet-4-5-20250929",
216 "max_tokens": 256,
217 "messages": [{"role": "user", "content": $(jq -Rs . < /tmp/area_prompt_final.txt)}]
218 }
219 EOF
220 )
221
222 RESPONSE=$(< /tmp/area_response.json)
223
224 if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
225 echo "HTTP Error: $HTTP_CODE"
226 echo "$RESPONSE" | jq . 2>/dev/null || echo "$RESPONSE"
227 exit 1
228 fi
229
230 if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then
231 echo "API Error:"
232 echo "$RESPONSE" | jq .
233 exit 1
234 fi
235
236 AREA_RESULT=$(echo "$RESPONSE" | jq -r '.content[0].text // empty')
237
238 if [ -z "$AREA_RESULT" ]; then
239 echo "Error: No response from Claude for area detection"
240 echo "$RESPONSE" | jq .
241 exit 1
242 fi
243
244 echo "Area detection result: $AREA_RESULT"
245
246 # Extract just the areas array, handling potential markdown fences
247 # shellcheck disable=SC2016
248 CLEAN_JSON=$(echo "$AREA_RESULT" | sed 's/^```json//; s/^```//; s/```$//' | tr -d '\n')
249 AREAS=$(echo "$CLEAN_JSON" | jq -r '.areas // [] | join(",")')
250 echo "Detected areas: $AREAS"
251
252 echo "detected_areas=$AREAS" >> "$GITHUB_OUTPUT"
253
254 INPUT_TOKENS=$(echo "$RESPONSE" | jq -r '.usage.input_tokens')
255 OUTPUT_TOKENS=$(echo "$RESPONSE" | jq -r '.usage.output_tokens')
256 echo "Pass 1 token usage - Input: $INPUT_TOKENS, Output: $OUTPUT_TOKENS"
257
258 # ========================================================================
259 # Use detected areas to filter magnets and search for candidates
260 # ========================================================================
261 - name: Filter magnets and search for candidates
262 if: steps.fetch-issue.outputs.should_continue == 'true'
263 id: gather-candidates
264 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
265 with:
266 github-token: ${{ steps.get-app-token.outputs.token }}
267 script: |
268 // ============================================================
269 // KNOWN DUPLICATE MAGNETS (from #46355)
270 // ============================================================
271 const DUPLICATE_MAGNETS = [
272 { number: 37074, title: "Support history with external ACP agents", areas: ["ai", "ai/gemini", "ai/acp"] },
273 { number: 35780, title: "Zed consumes a lot of memory and CPU when opening ~/ or other large file trees", areas: ["workspace", "performance", "performance/memory leak", "integrations/git"] },
274 { number: 16965, title: "Support for non UTF-8 text encodings", areas: ["editor", "internationalization"] },
275 { number: 38109, title: "Zed out of sync with changes made outside of editor", areas: ["workspace"] },
276 { number: 16727, title: "Select text in markdown preview", areas: ["preview/markdown", "languages/markdown"] },
277 { number: 31102, title: "RTL Right-to-Left Text Input/Rendering Support", areas: ["internationalization"] },
278 { number: 7371, title: "Restarts should be non-destructive on workspace restore/reload", areas: ["workspace", "serialization"] },
279 { number: 7992, title: "Font rendering on LoDPI displays", areas: ["ui/font"] },
280 { number: 40018, title: "Windows Beta: Terminal overwrites text when resized and window overflow", areas: ["integrations/terminal"] },
281 { number: 29962, title: "Agent Panel: Cannot access zed hosted models (via Cloudflare HKG)", areas: ["ai", "network"] },
282 { number: 15097, title: "Serialize undo history (local and remote projects)", areas: ["workspace", "serialization"] },
283 { number: 29846, title: "Collapsed code blocks are not restored properly", areas: ["editor", "serialization", "code folding"] },
284 { number: 38799, title: "Poor search performance in large repositories", areas: ["performance", "search"] },
285 { number: 27283, title: "Inefficient memory use when opening large file in Zed", areas: ["performance"] },
286 { number: 39806, title: "Raspberry Pi OS (Trixie) Zed 0.207.3 Video Memory Corruption on Start", areas: ["gpui"] },
287 { number: 29970, title: "Unable to download any extensions (due to potential DigitalOcean IP block or ISP block)", areas: ["network"] },
288 { number: 29026, title: "Ability to copy/paste files from the system file manager", areas: ["workspace"] },
289 { number: 7940, title: "Zed is sometimes unresponsive when the OS awakes from sleep", areas: ["workspace"] },
290 { number: 37025, title: "Failed to generate thread summary", areas: ["ai"] },
291 { number: 16156, title: "Support for project settings to enable/disable/control AI features", areas: ["ai", "settings"] },
292 { number: 24752, title: "Extra horizontal scrolling when inline blame is enabled with soft wrapping", areas: ["editor"] },
293 { number: 20970, title: "Excessive memory consumption on project search with large files present", areas: ["performance/memory leak", "search", "multi-buffer"] },
294 { number: 12176, title: "Only some ligatures are being applied", areas: ["ui/font", "settings"] },
295 { number: 13564, title: "blade: Text is rendered either too thick or too thin", areas: ["ui/font"] },
296 { number: 38901, title: "Terminal freezes in Linux session when Ctrl+C is pressed before exit", areas: ["controls/keybinds", "integrations/terminal"] },
297 { number: 20167, title: "Support unsetting default keybindings", areas: ["controls/keybinds"] },
298 { number: 25469, title: "Tracking - Linux non-QWERTY keyboard support", areas: ["controls/keybinds"] },
299 { number: 29598, title: "Manual refresh on unsupported filesystems (nfs, fuse, exfat) without inotify/fsevents", areas: ["project panel"] },
300 { number: 14428, title: "Ordering of search tokens in file finder fuzzy match", areas: ["file finder"] },
301 { number: 20771, title: "Workspace: Reload to respect the desktop/workspace Zed windows were in after reload", areas: ["workspace", "serialization"] },
302 { number: 7465, title: "Lines with RTL text aren't rendered correctly", areas: ["editor", "internationalization/rtl support", "parity/vscode"] },
303 { number: 16120, title: "Large files without newlines (all on one line) cause Zed to hang/crash", areas: ["editor"] },
304 { number: 22703, title: "Syntax aware folding (folds.scm support)", areas: ["editor", "tree-sitter"] },
305 { number: 38927, title: "Find & Replace memory leak on large files", areas: ["performance", "performance/memory leak"] },
306 { number: 4560, title: "Improve streaming search speed", areas: ["performance", "search"] },
307 { number: 14053, title: "Linux Shortcuts don't work with non-latin / international keyboard layouts", areas: ["internationalization", "controls/keybinds"] },
308 { number: 31637, title: "High memory consumption in Project Search with large codebases", areas: ["performance/memory leak", "search"] },
309 { number: 11744, title: "Incorrect spacing of terminal font", areas: ["ui/font", "integrations/terminal"] },
310 { number: 4746, title: "Terminal Nerd Font rendering incorrect line height", areas: ["ui/font", "integrations/terminal"] },
311 { number: 10647, title: "User configurable mouse bindings (like keymap for key+mouse)", areas: ["controls/keybinds", "controls/mouse", "accessibility"] },
312 { number: 34865, title: "ctrl-w with pane::CloseActiveItem binding closes the project panel instead of the active pane", areas: ["controls/keybinds", "ui/panel"] },
313 { number: 12163, title: "Cannot see list of installed extensions when offline / disconnected", areas: ["network"] },
314 { number: 44630, title: "Tables do not render all columns in markdown preview", areas: ["preview/markdown"] },
315 { number: 39435, title: "Windows: Low fps in many cases", areas: ["gpui"] },
316 { number: 36227, title: "Zed becomes unresponsive when closing", areas: ["workspace"] },
317 { number: 44962, title: "Can not open file in zed if filename includes (1)", areas: ["workspace"] },
318 { number: 32318, title: "Zed hangs after exiting sleep mode in Linux", areas: ["workspace"] },
319 { number: 5120, title: "Add options to hide title and status bar", areas: ["settings", "status bar"] },
320 { number: 29323, title: "uv: Failed to detect Python venv correctly", areas: ["language server", "languages/python", "integrations/environment"] },
321 { number: 7450, title: "Support LSP Semantic Tokens", areas: ["language server", "languages", "ui/themes"] },
322 { number: 31846, title: "LSP: triggerCharacters for signature help declared by servers do not seem to be respected", areas: ["language server"] },
323 { number: 32792, title: "[SWAY] Zed window flashes rapidly on Sway/wlroots", areas: ["gpui"] },
324 { number: 28398, title: "Stale buffers should be removed from search multibuffer", areas: ["search", "multi-buffer"] },
325 { number: 35011, title: "Delete Key against remote Hosts Doesn't Delete Folders", areas: ["project panel"] },
326 { number: 8626, title: "Palette File Navigation - Preview File Content", areas: ["file finder"] },
327 { number: 31468, title: "Certain LSP features are not activated till you trigger them manually when working with a remote project", areas: ["language server/server failure", "autocompletions"] },
328 { number: 9789, title: "Zed checks for LSP updates when offline and disables LSPs irreversibly in the process", areas: ["language server/server failure"] },
329 { number: 21403, title: "Completions and code actions should not use uniform lists", areas: ["autocompletions", "popovers", "diagnostics"] },
330 { number: 15196, title: "Remote Project REPL support", areas: ["repl"] },
331 ];
332
333 const MAX_SEARCHES = 5;
334
335 const issueNumber = parseInt('${{ steps.fetch-issue.outputs.issue_number }}', 10);
336 const title = process.env.ISSUE_TITLE || '';
337 const body = process.env.ISSUE_BODY || '';
338 const detectedAreasStr = '${{ steps.detect-areas.outputs.detected_areas }}';
339 const detectedAreas = new Set(detectedAreasStr.split(',').filter(a => a.trim()));
340
341 console.log(`Detected areas from Claude: ${[...detectedAreas].join(', ') || '(none)'}`);
342
343 // Helper: check if two areas match (handles hierarchy like "ai" matching "ai/gemini")
344 function areasMatch(detected, magnetArea) {
345 if (detected === magnetArea) return true;
346 if (magnetArea.startsWith(detected + '/')) return true;
347 if (detected.startsWith(magnetArea + '/')) return true;
348 return false;
349 }
350
351 // Filter magnets based on detected areas
352 const relevantMagnets = DUPLICATE_MAGNETS.filter(magnet => {
353 if (detectedAreas.size === 0) return true;
354 return magnet.areas.some(magnetArea =>
355 [...detectedAreas].some(detected => areasMatch(detected, magnetArea))
356 );
357 }).slice(0, 20);
358
359 console.log(`Relevant duplicate magnets: ${relevantMagnets.length}`);
360
361 // Build search queries
362 const searchQueries = [];
363 const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
364
365 // 1. Keyword search from title
366 const stopwords = ['with', 'that', 'this', 'from', 'have', 'been', 'were', 'what', 'when',
367 'where', 'which', 'while', 'does', 'doesn', 'should', 'would', 'could',
368 'about', 'after', 'before', 'between', 'into', 'through', 'during',
369 'above', 'below', 'under', 'again', 'further', 'then', 'once', 'here',
370 'there', 'some', 'such', 'only', 'same', 'than', 'very', 'just', 'also',
371 'work', 'working', 'works', 'issue', 'problem', 'error', 'bug', 'zed'];
372 const titleKeywords = title
373 .toLowerCase()
374 .replace(/[^\w\s]/g, ' ')
375 .split(/\s+/)
376 .filter(w => w.length >= 3 && !stopwords.includes(w))
377 .slice(0, 5);
378
379 if (titleKeywords.length >= 2) {
380 searchQueries.push({
381 type: 'keyword',
382 query: `repo:zed-industries/zed is:issue created:>${thirtyDaysAgo} ${titleKeywords.join(' ')}`
383 });
384 }
385
386 // 2. Area-based searches (using Claude-detected areas)
387 for (const area of [...detectedAreas].slice(0, 3)) {
388 searchQueries.push({
389 type: 'area',
390 query: `repo:zed-industries/zed is:issue is:open label:"area:${area}" created:>${thirtyDaysAgo}`
391 });
392 }
393
394 // 3. Look for error patterns in the body
395 const errorPatterns = body.match(/(?:error|panic|crash|failed|exception)[:\s]+[^\n]{10,100}/gi) || [];
396 if (errorPatterns.length > 0) {
397 const errorSnippet = errorPatterns[0]
398 .slice(0, 60)
399 .replace(/[^\w\s]/g, ' ')
400 .replace(/\s+/g, ' ')
401 .trim();
402 if (errorSnippet.length > 15) {
403 searchQueries.push({
404 type: 'error',
405 query: `repo:zed-industries/zed is:issue "${errorSnippet.slice(0, 40)}"`
406 });
407 }
408 }
409
410 // Execute searches and collect candidates
411 const candidates = [];
412 const seenIssues = new Set([issueNumber]);
413
414 for (const { type, query } of searchQueries.slice(0, MAX_SEARCHES)) {
415 try {
416 console.log(`Search (${type}): ${query}`);
417 const { data: results } = await github.rest.search.issuesAndPullRequests({
418 q: query,
419 sort: 'created',
420 order: 'desc',
421 per_page: 10
422 });
423
424 for (const item of results.items) {
425 if (!seenIssues.has(item.number) && !item.pull_request) {
426 seenIssues.add(item.number);
427 candidates.push({
428 number: item.number,
429 title: item.title,
430 state: item.state,
431 created_at: item.created_at,
432 body_preview: (item.body || '').slice(0, 800),
433 source: type
434 });
435 }
436 }
437 } catch (error) {
438 console.log(`Search failed (${type}): ${error.message}`);
439 }
440 }
441
442 console.log(`Found ${candidates.length} candidates from searches`);
443
444 // Prepare issue data for Claude
445 const issueData = {
446 number: issueNumber,
447 title: title,
448 body: body.slice(0, 4000),
449 };
450
451 // Prepare output
452 core.setOutput('issue_data', JSON.stringify(issueData));
453 core.setOutput('duplicate_magnets', JSON.stringify(relevantMagnets));
454 core.setOutput('candidates', JSON.stringify(candidates.slice(0, 12)));
455 core.setOutput('detected_areas', [...detectedAreas].join(', '));
456 core.setOutput('should_analyze', (relevantMagnets.length > 0 || candidates.length > 0) ? 'true' : 'false');
457 env:
458 ISSUE_TITLE: ${{ steps.fetch-issue.outputs.issue_title }}
459 ISSUE_BODY: ${{ steps.fetch-issue.outputs.issue_body }}
460
461 # ========================================================================
462 # PASS 2: Analyze duplicates with Claude
463 # ========================================================================
464 - name: "Pass 2: Analyze duplicates with Claude"
465 if: |
466 steps.fetch-issue.outputs.should_continue == 'true' &&
467 steps.gather-candidates.outputs.should_analyze == 'true'
468 id: analyze
469 env:
470 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_ISSUE_DEDUP }}
471 ISSUE_DATA: ${{ steps.gather-candidates.outputs.issue_data }}
472 DUPLICATE_MAGNETS: ${{ steps.gather-candidates.outputs.duplicate_magnets }}
473 CANDIDATES: ${{ steps.gather-candidates.outputs.candidates }}
474 run: |
475 # shellcheck disable=SC2016
476 cat > /tmp/prompt.txt << 'PROMPT_EOF'
477 You are analyzing a GitHub issue to determine if it might be a duplicate of an existing issue.
478
479 ## New Issue Being Analyzed
480 ISSUE_DATA_PLACEHOLDER
481
482 ## Known Frequently-Duplicated Issues (High Priority)
483 These issues have historically received many duplicate reports. Check these first.
484 DUPLICATE_MAGNETS_PLACEHOLDER
485
486 ## Recent Similar Issues Found by Search
487 CANDIDATES_PLACEHOLDER
488
489 ## Your Task
490
491 1. First, understand what the new issue is about:
492 - What specific bug or problem is being reported?
493 - What error messages, stack traces, or specific behaviors are mentioned?
494 - What component/feature is affected?
495
496 2. Check against the frequently-duplicated issues first (high priority):
497 - These are known "duplicate magnets" that often get re-reported
498 - If the new issue describes the same problem, it's likely a duplicate
499
500 3. Then check the recent similar issues:
501 - Look for issues describing the SAME bug, not just related topics
502
503 ## Duplicate Criteria (be strict!)
504
505 An issue IS a duplicate if:
506 - It describes the EXACT same bug with the same root cause
507 - It has the same error message or stack trace
508 - It has the same reproduction steps leading to the same outcome
509
510 An issue is NOT a duplicate if:
511 - It's merely related to the same feature/area
512 - It has similar symptoms but potentially different causes
513 - It mentions similar things but describes a different problem
514
515 Be VERY conservative. It's better to miss a duplicate than to incorrectly flag a unique issue.
516
517 ## Response Format
518
519 Return ONLY a JSON object (no markdown fences, no explanation before or after):
520 {
521 "is_potential_duplicate": boolean,
522 "confidence": "high" | "medium" | "low" | "none",
523 "potential_duplicates": [
524 {"number": integer, "title": "string", "similarity_reason": "string explaining why this might be the same bug"}
525 ],
526 "analysis_summary": "Brief explanation of what the new issue is about and your conclusion",
527 "recommendation": "flag_as_duplicate" | "needs_human_review" | "not_a_duplicate"
528 }
529 PROMPT_EOF
530
531 # Single quotes are intentional to prevent bash expansion; node reads env vars via process.env
532 # shellcheck disable=SC2016
533 node << 'SCRIPT_EOF'
534 const fs = require('fs');
535
536 let prompt = fs.readFileSync('/tmp/prompt.txt', 'utf8');
537 prompt = prompt.replace('ISSUE_DATA_PLACEHOLDER', process.env.ISSUE_DATA);
538 prompt = prompt.replace('DUPLICATE_MAGNETS_PLACEHOLDER', process.env.DUPLICATE_MAGNETS);
539 prompt = prompt.replace('CANDIDATES_PLACEHOLDER', process.env.CANDIDATES);
540
541 fs.writeFileSync('/tmp/prompt_final.txt', prompt);
542 SCRIPT_EOF
543
544 HTTP_CODE=$(curl -s -w "%{http_code}" -o /tmp/response.json -X POST "https://api.anthropic.com/v1/messages" \
545 -H "Content-Type: application/json" \
546 -H "x-api-key: $ANTHROPIC_API_KEY" \
547 -H "anthropic-version: 2023-06-01" \
548 --data-binary @- << EOF
549 {
550 "model": "claude-sonnet-4-5-20250929",
551 "max_tokens": 1024,
552 "messages": [{"role": "user", "content": $(jq -Rs . < /tmp/prompt_final.txt)}]
553 }
554 EOF
555 )
556
557 RESPONSE=$(< /tmp/response.json)
558
559 if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
560 echo "HTTP Error: $HTTP_CODE"
561 echo "$RESPONSE" | jq . 2>/dev/null || echo "$RESPONSE"
562 exit 1
563 fi
564
565 if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then
566 echo "API Error:"
567 echo "$RESPONSE" | jq .
568 exit 1
569 fi
570
571 ANALYSIS=$(echo "$RESPONSE" | jq -r '.content[0].text // empty')
572
573 if [ -z "$ANALYSIS" ]; then
574 echo "Error: No response from Claude"
575 echo "$RESPONSE" | jq .
576 exit 1
577 fi
578
579 {
580 echo "analysis<<ANALYSIS_EOF"
581 echo "$ANALYSIS"
582 echo "ANALYSIS_EOF"
583 } >> "$GITHUB_OUTPUT"
584
585 INPUT_TOKENS=$(echo "$RESPONSE" | jq -r '.usage.input_tokens')
586 OUTPUT_TOKENS=$(echo "$RESPONSE" | jq -r '.usage.output_tokens')
587 echo "Pass 2 token usage - Input: $INPUT_TOKENS, Output: $OUTPUT_TOKENS"
588
589 # ========================================================================
590 # Log results
591 # ========================================================================
592 - name: Log analysis results
593 if: |
594 steps.fetch-issue.outputs.should_continue == 'true' &&
595 !cancelled()
596 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
597 with:
598 script: |
599 const issueNumber = parseInt('${{ steps.fetch-issue.outputs.issue_number }}', 10) || 0;
600 const issueTitle = process.env.ISSUE_TITLE || '';
601 const detectedAreas = '${{ steps.gather-candidates.outputs.detected_areas }}' || '(none)';
602 const shouldAnalyze = '${{ steps.gather-candidates.outputs.should_analyze }}' === 'true';
603 const analysisRaw = process.env.ANALYSIS_OUTPUT || '';
604
605 console.log('='.repeat(60));
606 console.log('DUPLICATE DETECTION RESULTS (TWO-PASS)');
607 console.log('='.repeat(60));
608 console.log(`Issue: #${issueNumber} - ${issueTitle}`);
609 console.log(`URL: https://github.com/zed-industries/zed/issues/${issueNumber}`);
610 console.log(`Detected Areas: ${detectedAreas}`);
611
612 if (!shouldAnalyze) {
613 console.log('\nNo duplicate magnets or candidates found - skipping analysis');
614 core.summary.addHeading(`✅ Issue #${issueNumber}: No similar issues found`, 2);
615 core.summary.addRaw(`\n**Title:** ${issueTitle}\n\n`);
616 core.summary.addRaw(`**Detected Areas:** ${detectedAreas}\n\n`);
617 core.summary.addRaw('No potential duplicates were found by search or in the known duplicate magnets list.\n');
618 await core.summary.write();
619 return;
620 }
621
622 if (!analysisRaw) {
623 console.log('\nNo analysis output received');
624 core.summary.addHeading(`⚠️ Issue #${issueNumber}: Analysis incomplete`, 2);
625 core.summary.addRaw(`**Detected Areas:** ${detectedAreas}\n\n`);
626 core.summary.addRaw('The Claude analysis step did not produce output. Check workflow logs.\n');
627 await core.summary.write();
628 return;
629 }
630
631 try {
632 let cleanJson = analysisRaw.trim();
633 if (cleanJson.startsWith('```')) {
634 cleanJson = cleanJson.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
635 }
636
637 const analysis = JSON.parse(cleanJson);
638
639 console.log(`\nIs Potential Duplicate: ${analysis.is_potential_duplicate}`);
640 console.log(`Confidence: ${analysis.confidence}`);
641 console.log(`Recommendation: ${analysis.recommendation}`);
642 console.log(`\nAnalysis Summary:\n${analysis.analysis_summary}`);
643
644 if (analysis.potential_duplicates && analysis.potential_duplicates.length > 0) {
645 console.log(`\nPotential Duplicates Found: ${analysis.potential_duplicates.length}`);
646 for (const dup of analysis.potential_duplicates) {
647 console.log(` - #${dup.number}: ${dup.title}`);
648 console.log(` Reason: ${dup.similarity_reason}`);
649 }
650 } else {
651 console.log('\nNo potential duplicates identified by analysis.');
652 }
653
654 console.log('\n' + '='.repeat(60));
655
656 const summaryIcon = analysis.is_potential_duplicate ? '⚠️' : '✅';
657 const summaryText = analysis.is_potential_duplicate
658 ? `Potential duplicate detected (${analysis.confidence} confidence)`
659 : 'No likely duplicates found';
660
661 core.summary.addHeading(`${summaryIcon} Issue #${issueNumber}: ${summaryText}`, 2);
662 core.summary.addRaw(`\n**Title:** ${issueTitle}\n\n`);
663 core.summary.addRaw(`**Detected Areas:** ${detectedAreas}\n\n`);
664 core.summary.addRaw(`**Recommendation:** \`${analysis.recommendation}\`\n\n`);
665 core.summary.addRaw(`**Summary:** ${analysis.analysis_summary}\n\n`);
666
667 if (analysis.potential_duplicates && analysis.potential_duplicates.length > 0) {
668 core.summary.addHeading('Potential Duplicates', 3);
669 const rows = analysis.potential_duplicates.map(d => [
670 `[#${d.number}](https://github.com/zed-industries/zed/issues/${d.number})`,
671 d.title.slice(0, 60) + (d.title.length > 60 ? '...' : ''),
672 d.similarity_reason
673 ]);
674 core.summary.addTable([
675 [{data: 'Issue', header: true}, {data: 'Title', header: true}, {data: 'Similarity Reason', header: true}],
676 ...rows
677 ]);
678 }
679
680 await core.summary.write();
681
682 } catch (e) {
683 console.log('Failed to parse analysis output:', e.message);
684 console.log('Raw output:', analysisRaw);
685 core.summary.addHeading(`⚠️ Issue #${issueNumber}: Failed to parse analysis`, 2);
686 core.summary.addRaw(`**Detected Areas:** ${detectedAreas}\n\n`);
687 core.summary.addRaw(`Error: ${e.message}\n\nRaw output:\n\`\`\`\n${analysisRaw.slice(0, 1000)}\n\`\`\``);
688 await core.summary.write();
689 }
690 env:
691 ISSUE_TITLE: ${{ steps.fetch-issue.outputs.issue_title }}
692 ANALYSIS_OUTPUT: ${{ steps.analyze.outputs.analysis }}