[Feature]: Real debrid in cloud integration #353
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: Triage (needs-info) | |
| on: | |
| issues: | |
| types: [opened, edited, reopened] | |
| permissions: | |
| issues: write | |
| jobs: | |
| needs_info: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Label low-context issues | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const issue_number = context.payload.issue.number; | |
| const issue = context.payload.issue; | |
| const title = (issue.title || "").trim(); | |
| const body = issue.body || ""; | |
| const labels = (issue.labels || []).map(l => (typeof l === "string" ? l : l.name).toLowerCase()); | |
| const NEEDS_INFO = "needs-info"; | |
| const NEEDS_INFO_COLOR = "d4c5f9"; | |
| const NEEDS_INFO_DESC = "More details needed to reproduce / triage."; | |
| function hasLabel(name) { | |
| return labels.includes(name.toLowerCase()); | |
| } | |
| function extractSection(title) { | |
| const re = new RegExp(`^###\\s+${title.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}\\s*$`, "m"); | |
| const match = body.match(re); | |
| if (!match) return ""; | |
| const start = match.index + match[0].length; | |
| const rest = body.slice(start); | |
| const next = rest.search(/^###\s+/m); | |
| const section = (next === -1 ? rest : rest.slice(0, next)); | |
| return section.trim(); | |
| } | |
| function extractFirstSection(titles) { | |
| for (const sectionTitle of titles) { | |
| const value = extractSection(sectionTitle); | |
| if (value) return value; | |
| } | |
| return ""; | |
| } | |
| function normalizeText(value) { | |
| return (value || "").replace(/\s+/g, " ").trim(); | |
| } | |
| function stripIssuePrefix(value) { | |
| return normalizeText(value).replace(/^\[[^\]]+\]:\s*/i, "").trim(); | |
| } | |
| const steps = extractSection("Steps to reproduce"); | |
| const expected = extractSection("Expected behavior"); | |
| const actual = extractSection("Actual behavior"); | |
| const logs = extractFirstSection([ | |
| "Logs (required for crash reports)", | |
| "Logs (optional but helpful)", | |
| ]); | |
| const extra = extractSection("Anything else? (optional)"); | |
| const summaryTitle = stripIssuePrefix(title); | |
| const looksLikeBugForm = !!(steps || expected || actual); | |
| const isBugIssue = hasLabel("bug") || looksLikeBugForm; | |
| const isFeatureIssue = | |
| hasLabel("enhancement") || | |
| hasLabel("feature") || | |
| hasLabel("feature request"); | |
| if (!isBugIssue || isFeatureIssue) { | |
| return; | |
| } | |
| const problems = []; | |
| const genericTitle = /^(bug|issue|problem|help|question|crash|broken|error|bug report|short summary here|title here)$/i; | |
| const numericOnlyTitle = /^#?\d+$/; | |
| const crashPattern = /\b(crash|crashes|crashed|crashing|force close|force closes|force closed|fatal exception|app closes|app closed unexpectedly)\b/i; | |
| const crashContext = [summaryTitle, steps, actual, extra].map(normalizeText).join("\n"); | |
| const isCrashIssue = crashPattern.test(crashContext); | |
| const normalizedLogs = normalizeText(logs); | |
| const hasLogs = normalizedLogs.length >= 20 && !/^(n\/a|na|none|no|not available)$/i.test(normalizedLogs); | |
| if (!summaryTitle || summaryTitle.length < 8 || genericTitle.test(summaryTitle) || numericOnlyTitle.test(summaryTitle)) { | |
| problems.push("Issue title (replace the default `[Bug]:` prefix with a short summary of the actual problem)"); | |
| } | |
| if (!steps || steps.length < 30) problems.push("Steps to reproduce (please list exact steps)"); | |
| if (!expected || expected.length < 10) problems.push("Expected behavior"); | |
| if (!actual || actual.length < 10) problems.push("Actual behavior (include any on-screen error text)"); | |
| if (isCrashIssue && !hasLogs) { | |
| problems.push("Logs (required for crash reports; include a log snippet or stack trace)"); | |
| } | |
| async function ensureLabel(name, color, description) { | |
| try { | |
| await github.rest.issues.getLabel({ owner, repo, name }); | |
| } catch (e) { | |
| try { | |
| await github.rest.issues.createLabel({ owner, repo, name, color, description }); | |
| } catch (_) {} | |
| } | |
| } | |
| const hasNeedsInfo = hasLabel(NEEDS_INFO); | |
| if (problems.length > 0) { | |
| await ensureLabel(NEEDS_INFO, NEEDS_INFO_COLOR, NEEDS_INFO_DESC); | |
| if (!hasNeedsInfo) { | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number, | |
| labels: [NEEDS_INFO], | |
| }); | |
| } | |
| const marker = "<!-- nuvio-bot:needs-info -->"; | |
| const commentBody = | |
| `${marker}\n` + | |
| `Thanks for the report. Could you add a bit more detail so we can reproduce it?\n\n` + | |
| `Missing / too short:\n` + | |
| problems.map(p => `- ${p}`).join("\n") + | |
| `\n\n` + | |
| `Use a specific title, for example: \`[Bug]: Playback freezes when switching audio tracks on iOS\`.\n` + | |
| `${isCrashIssue ? `Crash reports must include logs.\n` : `Logs are optional for most issues, but they help a lot.\n`}`; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, | |
| repo, | |
| issue_number, | |
| per_page: 100, | |
| }); | |
| const alreadyCommented = comments.some(c => (c.body || "").includes(marker)); | |
| if (!alreadyCommented) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number, | |
| body: commentBody, | |
| }); | |
| } | |
| } else if (hasNeedsInfo) { | |
| try { | |
| await github.rest.issues.removeLabel({ owner, repo, issue_number, name: NEEDS_INFO }); | |
| } catch (_) {} | |
| } |