|
1 | | -name: Similar Issues via AI MCP |
| 1 | +name: duplicate-issues |
2 | 2 |
|
3 | 3 | on: |
4 | 4 | issues: |
5 | 5 | types: [opened] |
| 6 | + workflow_dispatch: |
| 7 | + inputs: |
| 8 | + issue_number: |
| 9 | + description: Issue number to check manually |
| 10 | + required: true |
| 11 | + type: number |
6 | 12 |
|
7 | 13 | jobs: |
8 | | - find-similar: |
| 14 | + check-duplicates: |
| 15 | + runs-on: ubuntu-latest |
9 | 16 | permissions: |
10 | 17 | contents: read |
11 | 18 | issues: write |
12 | | - models: read |
13 | | - runs-on: ubuntu-slim |
14 | 19 | steps: |
15 | | - - name: Check out repository |
| 20 | + - name: Checkout repository |
16 | 21 | uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
17 | 22 |
|
18 | | - - name: Prepare prompt variables |
19 | | - id: prepare_input |
20 | | - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 |
21 | | - with: |
22 | | - script: | |
23 | | - const issue = context.payload.issue || {}; |
24 | | - const title = issue.title || ''; |
25 | | - const body = issue.body || ''; |
26 | | - const indent = ' '; |
27 | | - // Indent subsequent lines so YAML block scalar indentation remains valid |
28 | | - const bodyIndented = body.replace(/\n/g, '\n' + indent); |
29 | | - core.setOutput('issue_title_json', JSON.stringify(title)); |
30 | | - core.setOutput('issue_body_indented_json', JSON.stringify(bodyIndented)); |
31 | | - core.setOutput('issue_number', issue.number); |
| 23 | + - name: Set up Bun |
| 24 | + uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 |
32 | 25 |
|
33 | | - - name: Find similar issues with AI (MCP) |
34 | | - id: inference |
35 | | - uses: actions/ai-inference@a380166897b5408b8fb7dddd148142794cb5624a # v2.0.6 |
36 | | - with: |
37 | | - prompt-file: ./.github/prompts/similar_issues.prompt.yml |
38 | | - input: | |
39 | | - issue_title: ${{ steps.prepare_input.outputs.issue_title_json }} |
40 | | - issue_body: ${{ steps.prepare_input.outputs.issue_body_indented_json }} |
41 | | - issue_number: ${{ steps.prepare_input.outputs.issue_number }} |
42 | | - repository: ${{ github.repository }} |
43 | | - enable-github-mcp: true |
44 | | - # Inference token can use GITHUB_TOKEN. MCP specifically requires a PAT. |
45 | | - token: ${{ secrets.GITHUB_TOKEN }} |
46 | | - github-mcp-token: ${{ secrets.USER_PAT }} |
47 | | - max-tokens: 8000 |
| 26 | + - name: Install opencode |
| 27 | + run: curl -fsSL https://opencode.ai/install | bash |
48 | 28 |
|
49 | | - - name: Prepare comment body |
50 | | - id: prepare |
51 | | - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 |
| 29 | + - name: Check for duplicate issues |
52 | 30 | env: |
53 | | - AI_RESPONSE: ${{ steps.inference.outputs.response }} |
54 | | - with: |
55 | | - script: | |
56 | | - let data; |
57 | | - try { |
58 | | - data = JSON.parse(process.env.AI_RESPONSE || '{}'); |
59 | | - } catch (e) { |
60 | | - core.setOutput('has_matches', 'false'); |
61 | | - return; |
| 31 | + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} |
| 32 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 33 | + ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue_number }} |
| 34 | + OPENCODE_PERMISSION: | |
| 35 | + { |
| 36 | + "bash": { |
| 37 | + "*": "deny", |
| 38 | + "gh issue*": "allow" |
| 39 | + }, |
| 40 | + "webfetch": "deny" |
62 | 41 | } |
63 | | - const matches = Array.isArray(data.matches) ? data.matches.filter(m => m.number !== context.payload.issue.number) : []; |
64 | | - if (!matches.length) { |
65 | | - core.setOutput('has_matches', 'false'); |
66 | | - return; |
67 | | - } |
68 | | - const lines = []; |
69 | | - lines.push('I found similar issues that might help:'); |
70 | | - for (const m of matches.slice(0, 3)) { |
71 | | - const num = m.number != null ? `#${m.number}` : ''; |
72 | | - const title = m.title || 'Untitled'; |
73 | | - const url = m.url || ''; |
74 | | - const score = typeof m.similarity_score === 'number' ? ` (similarity: ${m.similarity_score.toFixed(2)})` : ''; |
75 | | - lines.push(`- ${url}${score}`.trim()); |
76 | | - } |
77 | | - core.setOutput('has_matches', 'true'); |
78 | | - core.setOutput('comment_body', lines.join('\n')); |
| 42 | + run: | |
| 43 | + if [ -z "$ISSUE_NUMBER" ]; then |
| 44 | + echo "issue_number is required" |
| 45 | + exit 1 |
| 46 | + fi |
| 47 | +
|
| 48 | + opencode run -m ${{ vars.OPENCODE_MODEL }} "A new issue has been created:' |
| 49 | +
|
| 50 | + Issue number: |
| 51 | + $ISSUE_NUMBER |
| 52 | +
|
| 53 | + Lookup this issue and search through existing issues (excluding #$ISSUE_NUMBER) in this repository to find any potential duplicates of this new issue. |
| 54 | + Consider: |
| 55 | + 1. Similar titles or descriptions |
| 56 | + 2. Same error messages or symptoms |
| 57 | + 3. Related functionality or components |
| 58 | + 4. Similar feature requests |
| 59 | +
|
| 60 | + If you find any potential duplicates, please comment on the new issue with: |
| 61 | + - A brief explanation of why it might be a duplicate |
| 62 | + - Links to the potentially duplicate issues |
| 63 | + - A suggestion to check those issues first |
| 64 | +
|
| 65 | + Use this format for the comment: |
| 66 | + 'This issue might be a duplicate of existing issues. Please check: |
| 67 | + - #[issue_number]: [brief description of similarity] |
| 68 | +
|
| 69 | + Feel free to ignore if none of these address your specific case.' |
79 | 70 |
|
80 | | - - name: Comment similar issues |
81 | | - if: steps.prepare.outputs.has_matches == 'true' |
82 | | - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 |
83 | | - with: |
84 | | - script: | |
85 | | - const body = ${{ toJson(steps.prepare.outputs.comment_body) }}; |
86 | | - await github.rest.issues.createComment({ |
87 | | - owner: context.repo.owner, |
88 | | - repo: context.repo.repo, |
89 | | - issue_number: context.payload.issue.number, |
90 | | - body |
91 | | - }); |
| 71 | + If no clear duplicates are found, do not comment." |
0 commit comments