Security: predictable /tmp/nc_exit_status in postdetails - symlink clobber and TOCTOU #11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: AI Issue Triage | |
| on: | |
| issues: | |
| types: | |
| - opened | |
| - edited | |
| # Note: This workflow uses GitHub Models which may not be available | |
| # in all environments. If GitHub Models API returns 401 or is unavailable, | |
| # the workflow will skip gracefully and the issue will still be processed | |
| # by other automation (labeler, etc.) | |
| permissions: | |
| issues: write | |
| contents: read | |
| models: read | |
| jobs: | |
| ai-triage: | |
| if: github.repository_owner == 'GameServerManagers' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Triage issue with GitHub Models | |
| uses: actions/github-script@v7 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| script: | | |
| const title = context.payload.issue.title || ''; | |
| const body = context.payload.issue.body || ''; | |
| const number = context.payload.issue.number; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const AI_MARKER = '<!-- ai-triage -->'; | |
| function parseTriageResponse(raw) { | |
| const input = (raw || '').trim(); | |
| if (!input) return {}; | |
| const candidates = [input]; | |
| const fenced = input.match(/```(?:json)?\s*([\s\S]*?)```/i); | |
| if (fenced?.[1]) candidates.push(fenced[1].trim()); | |
| const firstBrace = input.indexOf('{'); | |
| const lastBrace = input.lastIndexOf('}'); | |
| if (firstBrace !== -1 && lastBrace > firstBrace) { | |
| candidates.push(input.slice(firstBrace, lastBrace + 1)); | |
| } | |
| for (const candidate of candidates) { | |
| try { | |
| return JSON.parse(candidate); | |
| } catch (_err) { | |
| // Continue trying fallbacks. | |
| } | |
| } | |
| return {}; | |
| } | |
| // For short bodies, apply "needs: more info" label directly. | |
| // Skip the AI call but still label the issue. | |
| const isShortBody = body.trim().length < 80; | |
| if (isShortBody) { | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner, repo, issue_number: number, | |
| labels: ['needs: more info'], | |
| }); | |
| } catch (err) { | |
| console.log('Could not apply label for short body:', err.message); | |
| } | |
| return; | |
| } | |
| // ── Call GitHub Models ──────────────────────────────────────── | |
| // Note: GitHub Models access may not be available in all environments. | |
| // If the API returns 401 (Unauthorized), it means GitHub Models is not | |
| // enabled for this repository or the current token lacks access. | |
| // The workflow will gracefully skip AI triage and continue. | |
| let triage; | |
| try { | |
| const res = await fetch( | |
| `https://models.github.ai/orgs/${owner}/inference/chat/completions`, | |
| { | |
| method: 'POST', | |
| headers: { | |
| 'Accept': 'application/vnd.github+json', | |
| 'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`, | |
| 'X-GitHub-Api-Version': '2026-03-10', | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| model: 'openai/gpt-4.1-mini', | |
| temperature: 0.1, | |
| max_tokens: 400, | |
| messages: [ | |
| { | |
| role: 'system', | |
| content: | |
| 'You are a triage assistant for LinuxGSM, an open-source ' + | |
| 'Linux game server manager. Your role is to:\n' + | |
| '1. Analyze issue quality (completeness, clarity)\n' + | |
| '2. Extract game names mentioned in the issue, even if misspelled or abbreviated\n' + | |
| '3. Suggest corrections for likely typos using fuzzy matching\n' + | |
| '4. Respond ONLY with a valid JSON object — no markdown fences.\n\n' + | |
| 'Common game name variations and typos you should recognize:\n' + | |
| '- "Valhiem" → "Valheim"\n' + | |
| '- "Rrust" → "Rust"\n' + | |
| '- "Conterstrike" / "CS" / "CSGO" → "Counter-Strike: Global Offensive"\n' + | |
| '- "Garrys" / "GMod" → "Garrys Mod"\n' + | |
| '- "ARK" / "Ark" → "ARK: Survival Evolved"\n' + | |
| '- "DayZ" / "Dayz" → "DayZ"\n' + | |
| '- "Insurgency Sandstorm" / "Insurgency 2" → "Insurgency: Sandstorm"', | |
| }, | |
| { | |
| role: 'user', | |
| content: | |
| `Title: ${title}\n\nBody:\n${body.slice(0, 3000)}\n\n` + | |
| 'Respond with this JSON schema:\n' + | |
| '{\n' + | |
| ' "quality": "good" | "ok" | "poor",\n' + | |
| ' "missing_info": ["list of specific missing fields"],\n' + | |
| ' "detected_game": "canonical game name if one is mentioned, or null",\n' + | |
| ' "game_confidence": "high" | "medium" | "low" | null,\n' + | |
| ' "game_note": "correction suggestion if the user misspelled a game name, or empty string",\n' + | |
| ' "comment": "one or two sentence note to the reporter, or empty string"\n' + | |
| '}', | |
| }, | |
| ], | |
| }), | |
| } | |
| ); | |
| // GitHub Models may return 401 if not available for this repository. | |
| // This is expected and not an error — the workflow simply skips AI triage. | |
| if (!res.ok) { | |
| console.log(`GitHub Models returned ${res.status} — skipping AI triage.`); | |
| return; | |
| } | |
| const data = await res.json(); | |
| const raw = data.choices?.[0]?.message?.content || '{}'; | |
| triage = parseTriageResponse(raw); | |
| } catch (err) { | |
| // Never fail the workflow if the AI call errors — it's advisory only. | |
| console.log('AI triage skipped:', err.message); | |
| return; | |
| } | |
| if (!triage || typeof triage !== 'object') { | |
| triage = {}; | |
| } | |
| // ── Act on the result ──────────────────────────────────────── | |
| const isPoor = triage.quality === 'poor'; | |
| const missing = Array.isArray(triage.missing_info) ? triage.missing_info : []; | |
| const hasIssues = isPoor || missing.length > 0; | |
| // Prepare labels to apply | |
| const labelsToApply = []; | |
| // Check if a game was detected with high confidence | |
| const detectedGame = triage.detected_game; | |
| const gameConfidence = triage.game_confidence; | |
| if (detectedGame && gameConfidence === 'high') { | |
| labelsToApply.push(`game: ${detectedGame}`); | |
| } | |
| // Apply "needs: more info" label if quality issues detected | |
| if (hasIssues) { | |
| labelsToApply.push('needs: more info'); | |
| } | |
| // Apply labels one-by-one so a single failure does not block all labels. | |
| const uniqueLabels = [...new Set(labelsToApply)]; | |
| for (const label of uniqueLabels) { | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: number, | |
| labels: [label], | |
| }); | |
| } catch (err) { | |
| console.log(`Could not apply label "${label}":`, err.message); | |
| } | |
| } | |
| // Post a comment only when there is something specific to say | |
| const gameNote = triage.game_note || ''; | |
| const reporterComment = triage.comment || ''; | |
| if (!hasIssues && !gameNote) return; | |
| const missingBlock = missing.length > 0 | |
| ? `\n\n**Missing information:**\n${missing.map(m => `- ${m}`).join('\n')}` | |
| : ''; | |
| const gameBlock = gameNote | |
| ? `\n\n**Game name note:** ${gameNote}` | |
| : ''; | |
| const triageCommentBody = | |
| `${AI_MARKER}\n` + | |
| `Thanks for opening this issue! 👋\n\n` + | |
| `${reporterComment}` + | |
| `${missingBlock}` + | |
| `${gameBlock}\n\n` + | |
| `_This note was generated automatically by AI triage and may not be perfect. ` + | |
| `A maintainer will review shortly._`; | |
| try { | |
| const comments = await github.rest.issues.listComments({ | |
| owner, | |
| repo, | |
| issue_number: number, | |
| per_page: 100, | |
| }); | |
| const existingAiComment = comments.data.find( | |
| (comment) => comment.user?.type === 'Bot' && comment.body?.includes(AI_MARKER) | |
| ); | |
| if (existingAiComment) { | |
| await github.rest.issues.updateComment({ | |
| owner, | |
| repo, | |
| comment_id: existingAiComment.id, | |
| body: triageCommentBody, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: number, | |
| body: triageCommentBody, | |
| }); | |
| } | |
| } catch (err) { | |
| console.log('Could not post comment:', err.message); | |
| } |