mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
fix: harden issue triage automation
This commit is contained in:
parent
cc59387588
commit
fe1bc880b2
5 changed files with 89 additions and 13 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -13,7 +13,7 @@ body:
|
|||
Please replace the default title with a short summary of the actual problem.
|
||||
|
||||
Vague reports such as "not working", "broken", or "same issue" may be labeled `needs-info` and closed after 1 day if the missing details are not added.
|
||||
Bug reports left open for more than 30 days may be closed as stale.
|
||||
Bug reports with no activity for more than 30 days may be closed as stale.
|
||||
If the app crashes or force closes, logs are required. Crash reports without logs will be closed.
|
||||
|
||||
- type: markdown
|
||||
|
|
|
|||
27
.github/workflows/close-stale-issues.yml
vendored
27
.github/workflows/close-stale-issues.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Close stale bug issues
|
||||
name: Close inactive bug issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
close_stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close bug issues open longer than 30 days
|
||||
- name: Close bug issues inactive longer than 30 days
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
|
|
@ -35,15 +35,32 @@ jobs:
|
|||
.slice(0, 10);
|
||||
|
||||
const items = await github.paginate(github.rest.search.issuesAndPullRequests, {
|
||||
q: `repo:${owner}/${repo} is:issue is:open label:bug created:<${cutoff}`,
|
||||
// Staleness is inactivity, not issue age. Do not close an old issue
|
||||
// that has had recent discussion or edits.
|
||||
q: `repo:${owner}/${repo} is:issue is:open label:bug updated:<${cutoff}`,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
core.info(`Found ${items.length} open bug issues older than ${CLOSE_AFTER_DAYS} days.`);
|
||||
core.info(`Found ${items.length} bug issues inactive for ${CLOSE_AFTER_DAYS} days.`);
|
||||
|
||||
async function hasBeenReopened(issue_number) {
|
||||
const events = await github.paginate(github.rest.issues.listEvents, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
per_page: 100,
|
||||
});
|
||||
return events.some(event => event.event === "reopened");
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const issue_number = item.number;
|
||||
|
||||
if (await hasBeenReopened(issue_number)) {
|
||||
core.info(`#${issue_number}: skipping because it was manually reopened.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
core.info(`#${issue_number}: would comment and close.`);
|
||||
continue;
|
||||
|
|
@ -63,7 +80,7 @@ jobs:
|
|||
if (!alreadyCommented) {
|
||||
const body =
|
||||
`${closeMarker}\n` +
|
||||
`Closing this bug report because it has been open for more than 30 days.\n\n` +
|
||||
`Closing this bug report because it has had no activity for more than 30 days.\n\n` +
|
||||
`If this is still relevant, please open a fresh issue with the latest details.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
|
|
|
|||
15
.github/workflows/close-unlabeled-issues.yml
vendored
15
.github/workflows/close-unlabeled-issues.yml
vendored
|
|
@ -36,9 +36,24 @@ jobs:
|
|||
|
||||
core.info(`Found ${items.length} open unlabeled issues.`);
|
||||
|
||||
async function hasBeenReopened(issueNumber) {
|
||||
const events = await github.paginate(github.rest.issues.listEvents, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
return events.some(event => event.event === "reopened");
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const issueNumber = item.number;
|
||||
|
||||
if (await hasBeenReopened(issueNumber)) {
|
||||
core.info(`#${issueNumber}: skipping because it was manually reopened.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
core.info(`#${issueNumber}: would comment and close.`);
|
||||
continue;
|
||||
|
|
|
|||
17
.github/workflows/stale-needs-info.yml
vendored
17
.github/workflows/stale-needs-info.yml
vendored
|
|
@ -32,11 +32,28 @@ jobs:
|
|||
return github.paginate(github.rest.search.issuesAndPullRequests, { q, per_page: 100 });
|
||||
}
|
||||
|
||||
async function hasBeenReopened(issue_number) {
|
||||
const events = await github.paginate(github.rest.issues.listEvents, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
per_page: 100,
|
||||
});
|
||||
return events.some(event => event.event === "reopened");
|
||||
}
|
||||
|
||||
const items = await listOpenNeedsInfoIssues();
|
||||
for (const item of items) {
|
||||
const issue_number = item.number;
|
||||
const updatedAtMs = new Date(item.updated_at).getTime();
|
||||
|
||||
// Reopening is an explicit human decision that overrides automated
|
||||
// closure. Never close a currently open issue that was reopened.
|
||||
if (await hasBeenReopened(issue_number)) {
|
||||
core.info(`#${issue_number}: skipping because it was manually reopened.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
|
|
|
|||
41
.github/workflows/triage-needs-info.yml
vendored
41
.github/workflows/triage-needs-info.yml
vendored
|
|
@ -2,7 +2,9 @@ name: Triage (needs-info)
|
|||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
# 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
|
||||
|
|
@ -24,6 +26,19 @@ jobs:
|
|||
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.";
|
||||
|
|
@ -72,12 +87,14 @@ jobs:
|
|||
|
||||
function isEmptyish(value) {
|
||||
const text = normalizedMeaningfulText(value);
|
||||
return !text || /^(_?no response_?|n\/a|na|none|no|not available|-|\.)$/i.test(text);
|
||||
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|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);
|
||||
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) {
|
||||
|
|
@ -119,16 +136,26 @@ jobs:
|
|||
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, 50)) {
|
||||
if (!hasUsefulText(description, 20)) {
|
||||
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)");
|
||||
// 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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue