NuvioStreaming/.github/workflows/triage-needs-info.yml
2026-04-19 18:59:45 +05:30

154 lines
6.4 KiB
YAML

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 (_) {}
}