diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 48a9a8f11..1229aba48 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -9,10 +9,11 @@ body: value: | Thanks for reporting a bug. - If we can reproduce it, we can usually fix it. This form is just to get the basics in one place. + If we can reproduce it, we can usually fix it. Please describe the bug in enough detail that someone else can understand what failed without watching a video or guessing from the title. Please replace the default title with a short summary of the actual problem. - If the app crashes, logs are required. Crash reports without logs may be labeled `needs-info`. + Vague reports such as "not working", "broken", or "same issue" may be labeled `needs-info` and closed if the missing details are not added. + If the app crashes or force closes, logs are required. Crash reports without logs will be closed. - type: markdown attributes: @@ -121,11 +122,26 @@ body: validations: required: true + - type: textarea + id: description + attributes: + label: Bug description + description: | + Explain the problem in detail. Do not only write "not working" or "same issue". + Include what screen/flow you were using, what you tapped or tried to do, what changed on screen, and any visible error text. + placeholder: | + I was trying to ... + The app ... + I saw ... + This happens on ... + validations: + required: true + - type: textarea id: steps attributes: label: Steps to reproduce - description: Exact steps. If it depends on specific content, describe it (movie/series, season/episode, source/addon name) without sharing private links. + description: Exact steps, one action per line. If it depends on specific content, describe it (movie/series, season/episode, source/addon name) without sharing private links. placeholder: | 1. Open ... 2. Navigate to ... @@ -146,7 +162,7 @@ body: id: actual attributes: label: Actual behavior - placeholder: "What actually happened (include any on-screen error text/codes)." + placeholder: "What actually happened? Include any on-screen error text/codes and whether the app froze, crashed, ignored input, showed the wrong content, etc." validations: required: true @@ -197,7 +213,7 @@ body: attributes: label: Logs (required for crash reports) description: | - Required if the app crashes or force closes. + Required if the app crashes or force closes. Crash reports without logs will be closed. For other bug reports, logs are optional but still helpful. **Android:** `adb logcat -d | tail -n 300` diff --git a/.github/workflows/triage-needs-info.yml b/.github/workflows/triage-needs-info.yml index 1073a6783..2bea1d468 100644 --- a/.github/workflows/triage-needs-info.yml +++ b/.github/workflows/triage-needs-info.yml @@ -55,10 +55,37 @@ jobs: return (value || "").replace(/\s+/g, " ").trim(); } + function normalizedMeaningfulText(value) { + return normalizeText(value) + .replace(/^```[a-zA-Z0-9_-]*/i, "") + .replace(/```$/i, "") + .trim(); + } + function stripIssuePrefix(value) { return normalizeText(value).replace(/^\[[^\]]+\]:\s*/i, "").trim(); } + function isEmptyish(value) { + const text = normalizedMeaningfulText(value); + return !text || /^(_?no response_?|n\/a|na|none|no|not available|-|\.)$/i.test(text); + } + + function isVague(value) { + const text = normalizedMeaningfulText(value); + return /^(same|same issue|me too|not working|doesn't work|doesnt work|broken|bug|issue|problem|help|please fix|crash|crashes|it crashes|see video|see screenshot|as title)$/i.test(text); + } + + function hasUsefulText(value, minimumLength) { + const text = normalizedMeaningfulText(value); + return !isEmptyish(text) && !isVague(text) && text.length >= minimumLength; + } + + const description = extractFirstSection([ + "Bug description", + "Describe the bug", + "Detailed description", + ]); const steps = extractSection("Steps to reproduce"); const expected = extractSection("Expected behavior"); const actual = extractSection("Actual behavior"); @@ -69,7 +96,7 @@ jobs: const extra = extractSection("Anything else? (optional)"); const summaryTitle = stripIssuePrefix(title); - const looksLikeBugForm = !!(steps || expected || actual); + const looksLikeBugForm = !!(description || steps || expected || actual); const isBugIssue = hasLabel("bug") || looksLikeBugForm; const isFeatureIssue = hasLabel("enhancement") || @@ -84,20 +111,20 @@ jobs: 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 crashContext = [summaryTitle, description, 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); + const normalizedLogs = normalizedMeaningfulText(logs); + const hasLogs = normalizedLogs.length >= 20 && !isEmptyish(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)"); + if (!hasUsefulText(description, 50)) { + problems.push("Bug description (explain the problem in detail, not just \"not working\" or \"same issue\")"); } + if (!hasUsefulText(steps, 40)) problems.push("Steps to reproduce (please list exact steps)"); + if (!hasUsefulText(expected, 10)) problems.push("Expected behavior"); + if (!hasUsefulText(actual, 20)) problems.push("Actual behavior (include any on-screen error text)"); async function ensureLabel(name, color, description) { try { @@ -111,6 +138,50 @@ jobs: const hasNeedsInfo = hasLabel(NEEDS_INFO); + if (isCrashIssue && !hasLogs) { + 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 = ""; + 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: + `${marker}\n` + + `Closing this crash report because crash reports must include logs.\n\n` + + `Please open a new report, or ask us to reopen this one, with a log snippet or stack trace from around the crash.\n\n` + + `Useful examples:\n` + + `- Android: \`adb logcat -d | tail -n 300\`\n` + + `- iOS: crash log from Xcode Organizer or Console.app\n` + + `- Desktop: terminal/console output from around the crash`, + }); + } + await github.rest.issues.update({ + owner, + repo, + issue_number, + state: "closed", + state_reason: "not_planned", + }); + return; + } + if (problems.length > 0) { await ensureLabel(NEEDS_INFO, NEEDS_INFO_COLOR, NEEDS_INFO_DESC); if (!hasNeedsInfo) { @@ -125,12 +196,13 @@ jobs: const marker = ""; 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` + + `Thanks for the report. Could you add more detail so we can reproduce it?\n\n` + + `Missing / too vague:\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`}`; + `Reports that only say "not working", "broken", or "same issue" are not enough to debug.\n` + + `Logs are optional for most issues, but they help a lot. Crash reports without logs are closed automatically.\n`; const comments = await github.paginate(github.rest.issues.listComments, { owner,