| 1 | name: Auto-label issues |
| 2 | |
| 3 | # Classify new issues into area / platform / severity labels using the DeepSeek |
| 4 | # API (the project's own model). The model is constrained to a fixed label set — |
| 5 | # anything it returns outside the set is dropped — so it can't invent labels. |
| 6 | # Issues it can't place into any area get `needs-triage` for a human to look at. |
| 7 | # Version (v1/v2) labels are handled separately by issue-version-label.yml. |
| 8 | on: |
| 9 | issues: |
| 10 | types: [opened, reopened] |
| 11 | |
| 12 | permissions: |
| 13 | issues: write |
| 14 | |
| 15 | concurrency: |
| 16 | group: issue-autolabel-${{ github.event.issue.number }} |
| 17 | cancel-in-progress: true |
| 18 | |
| 19 | jobs: |
| 20 | classify: |
| 21 | runs-on: ubuntu-latest |
| 22 | steps: |
| 23 | - uses: actions/github-script@v9 |
| 24 | env: |
| 25 | DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} |
| 26 | with: |
| 27 | script: | |
| 28 | const AREA = ['agent','mcp','config','updater','provider','desktop','tui','skills','rendering']; |
| 29 | const PLATFORM = ['windows','macos','linux']; |
| 30 | const SEVERITY = ['crash','data-loss','security']; |
| 31 | const ALLOWED = new Set([...AREA, ...PLATFORM, ...SEVERITY]); |
| 32 | |
| 33 | const issue = context.payload.issue; |
| 34 | const title = issue.title || ''; |
| 35 | const body = (issue.body || '').slice(0, 4000); |
| 36 | |
| 37 | const system = [ |
| 38 | 'You categorize GitHub issues for Reasonix, a Go-based AI coding agent with a Wails desktop app and a terminal UI.', |
| 39 | 'Pick labels ONLY from these fixed sets. Never invent labels.', |
| 40 | 'area (0-2, the affected subsystem):', |
| 41 | ' agent: core agent loop / tool-calling / reasoning', |
| 42 | ' mcp: MCP servers, plugins, codegraph', |
| 43 | ' config: configuration, setup wizard, .toml/.env', |
| 44 | ' updater: auto-update, installer, release packaging', |
| 45 | ' provider: model providers, model selection/switching', |
| 46 | ' desktop: Wails desktop GUI', |
| 47 | ' tui: terminal UI / CLI', |
| 48 | ' skills: skills system', |
| 49 | ' rendering: terminal rendering / flicker / repaint', |
| 50 | 'platform (only if clearly specific to one OS): windows, macos, linux', |
| 51 | 'severity (only if clearly applicable):', |
| 52 | ' crash: app crashes, hangs, or freezes', |
| 53 | ' data-loss: loss of sessions, config, or history', |
| 54 | ' security: credential/secret exposure or a security flaw', |
| 55 | 'Be conservative: omit a label when unsure. The issue may be in Chinese.', |
| 56 | 'Reply with JSON only: {"area":[],"platform":[],"severity":[]}', |
| 57 | ].join('\n'); |
| 58 | |
| 59 | let labels = []; |
| 60 | try { |
| 61 | const res = await fetch('https://api.deepseek.com/chat/completions', { |
| 62 | method: 'POST', |
| 63 | headers: { |
| 64 | 'Authorization': `Bearer ${process.env.DEEPSEEK_API_KEY}`, |
| 65 | 'Content-Type': 'application/json', |
| 66 | }, |
| 67 | body: JSON.stringify({ |
| 68 | model: 'deepseek-chat', |
| 69 | temperature: 0, |
| 70 | response_format: { type: 'json_object' }, |
| 71 | messages: [ |
| 72 | { role: 'system', content: system }, |
| 73 | { role: 'user', content: `Title: ${title}\n\nBody:\n${body}` }, |
| 74 | ], |
| 75 | }), |
| 76 | }); |
| 77 | if (!res.ok) { |
| 78 | core.warning(`DeepSeek API ${res.status}: ${await res.text()}`); |
| 79 | return; |
| 80 | } |
| 81 | const data = await res.json(); |
| 82 | const parsed = JSON.parse(data.choices[0].message.content); |
| 83 | labels = [...(parsed.area || []), ...(parsed.platform || []), ...(parsed.severity || [])] |
| 84 | .filter(l => ALLOWED.has(l)); |
| 85 | } catch (e) { |
| 86 | core.warning(`Classification failed: ${e.message}`); |
| 87 | return; |
| 88 | } |
| 89 | |
| 90 | if (!labels.some(l => AREA.includes(l))) labels.push('needs-triage'); |
| 91 | |
| 92 | if (labels.length) { |
| 93 | await github.rest.issues.addLabels({ |
| 94 | ...context.repo, |
| 95 | issue_number: issue.number, |
| 96 | labels, |
| 97 | }); |
| 98 | core.info(`Applied: ${labels.join(', ')}`); |
| 99 | } |
| 100 |