mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
257 lines
11 KiB
YAML
257 lines
11 KiB
YAML
name: Triage (needs-info)
|
|
|
|
on:
|
|
issues:
|
|
# A reopen is an explicit human triage decision. Do not run automatic
|
|
# closure/needs-info rules again after a maintainer reopens an issue.
|
|
types: [opened, edited]
|
|
|
|
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());
|
|
|
|
// An edit can happen after a human has reopened an issue. Preserve
|
|
// that explicit triage decision instead of closing it again.
|
|
const events = await github.paginate(github.rest.issues.listEvents, {
|
|
owner,
|
|
repo,
|
|
issue_number,
|
|
per_page: 100,
|
|
});
|
|
if (events.some(event => event.event === "reopened")) {
|
|
core.info(`#${issue_number}: skipping triage because it was manually reopened.`);
|
|
return;
|
|
}
|
|
|
|
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 escapeRegExp(value) {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
function extractSection(title) {
|
|
const re = new RegExp(`^###\\s+${escapeRegExp(title)}\\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 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 ||
|
|
!/[\p{L}\p{N}]/u.test(text) ||
|
|
/^(_?no response_?|n\/a|na|none|no|not available|-|\.)$/i.test(text);
|
|
}
|
|
|
|
function isVague(value) {
|
|
const text = normalizedMeaningfulText(value);
|
|
return /^(same|same issue|same as above|see above|me too|not working|doesn't work|doesnt work|does not work|nothing happens|broken|bug|issue|problem|error|failed|failure|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");
|
|
const logs = extractFirstSection([
|
|
"Logs (required for crash reports)",
|
|
"Logs (optional but helpful)",
|
|
]);
|
|
const extra = extractSection("Anything else? (optional)");
|
|
const summaryTitle = stripIssuePrefix(title);
|
|
|
|
const looksLikeBugForm = !!(description || 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, description, steps, actual, extra].map(normalizeText).join("\n");
|
|
const isCrashIssue = crashPattern.test(crashContext);
|
|
const normalizedLogs = normalizedMeaningfulText(logs);
|
|
const hasLogs = normalizedLogs.length >= 20 && !isEmptyish(normalizedLogs);
|
|
const hasDetailedDescription = hasUsefulText(description, 80);
|
|
|
|
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 (!hasUsefulText(description, 20)) {
|
|
problems.push("Bug description (explain the problem in detail, not just \"not working\" or \"same issue\")");
|
|
}
|
|
// Some valid bugs only need one short action (for example, "Login" or
|
|
// "Open Continue Watching"). Judge the report as a whole instead of
|
|
// imposing arbitrary per-field character counts.
|
|
if (!hasUsefulText(steps, 5) && !hasDetailedDescription) {
|
|
problems.push("Steps to reproduce (please list exact steps)");
|
|
}
|
|
if (!hasUsefulText(expected, 3) && !hasDetailedDescription) {
|
|
problems.push("Expected behavior");
|
|
}
|
|
if (!hasUsefulText(actual, 3) && !hasDetailedDescription) {
|
|
problems.push("Actual behavior (include any on-screen error text)");
|
|
}
|
|
|
|
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 (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 = "<!-- nuvio-bot:crash-logs-required-close -->";
|
|
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) {
|
|
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 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` +
|
|
`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,
|
|
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 (_) {}
|
|
}
|