diff --git a/.github/scripts/issue-version-triage.cjs b/.github/scripts/issue-version-triage.cjs index 1c652232c7..6ae9b2d546 100644 --- a/.github/scripts/issue-version-triage.cjs +++ b/.github/scripts/issue-version-triage.cjs @@ -12,17 +12,22 @@ function escapeRegExp(value) { function extractSectionValue(body, heading, followingHeadings = []) { if (!body) return null; + // Hidden template instructions are not declarations. Strip them before + // locating the section as well as before interpreting its value. + const visibleBody = stripHTMLComments(body); const boundary = followingHeadings.length ? followingHeadings.map(escapeRegExp).join("|") : "[^\\n]+"; const pattern = new RegExp( - `^#+\\s*${escapeRegExp(heading)}\\s*$\\n+([\\s\\S]*?)(?=^#+\\s*(?:${boundary})\\s*$|$)`, + // With the multiline flag, $ also matches the end of the first value + // line. Only the next field or the actual end of input ends a section. + `^#+\\s*${escapeRegExp(heading)}\\s*$\\n+([\\s\\S]*?)(?=^#+\\s*(?:${boundary})\\s*$|(?![\\s\\S]))`, "im" ); - const match = body.match(pattern); + const match = visibleBody.match(pattern); if (!match) return null; - const value = match[1].trim(); - return value || null; + // An empty declared field differs from a legacy report with no field. + return match[1].trim(); } function stripHTMLComments(value) { diff --git a/.github/scripts/issue-version-triage.test.cjs b/.github/scripts/issue-version-triage.test.cjs index 271801bf29..ec648d3d5e 100644 --- a/.github/scripts/issue-version-triage.test.cjs +++ b/.github/scripts/issue-version-triage.test.cjs @@ -263,6 +263,96 @@ test("additional topic classification is explicit and fail-quiet for legacy form ); }); +test("topic classification reads later paragraphs after an empty first line", () => { + for (const firstLine of ["None", "", "_No response_"]) { + const body = [ + "### Additional actionable topics", firstLine, "", + "The second host also loses its Docker inventory.", "", + "### Pulse version", "6.5.0", + ].join("\n"); + assert.equal(triage.internals.classifyAdditionalActionableTopics(body), true); + } +}); + +test("topic classification stops at form fields but retains topic subheadings", () => { + const { classifyAdditionalActionableTopics } = triage.internals; + for (const newline of ["\n", "\r\n"]) { + for (const followingField of ["Pulse version", "Additional context", "Logs, screenshots, or diagnostics", "Confirmations"]) { + assert.equal(classifyAdditionalActionableTopics([ + "### Additional actionable topics", "None", "", + `### ${followingField}`, "An error or a second topic outside this field.", + ].join(newline)), false); + } + assert.equal(classifyAdditionalActionableTopics([ + "### Additional actionable topics", "None", "", + "### Separate backup problem", "Backups belong to the wrong host.", + "### Pulse version", "6.5.0", + ].join(newline)), true); + } +}); + +test("hidden template fields cannot declare topics or override feedback type", () => { + const { classifyAdditionalActionableTopics, classifyV6FeedbackType } = triage.internals; + const hidden = [""].join("\n"); + assert.equal(classifyAdditionalActionableTopics(hidden), null); + assert.equal(classifyV6FeedbackType(hidden), null); + assert.equal(classifyAdditionalActionableTopics( + `${hidden}\n### Additional actionable topics\nNone\n\n### Pulse version\n6.5.0` + ), false); + assert.equal(classifyV6FeedbackType( + `${hidden}\n### Feedback type\nDocumentation issue\n\n### Pulse version\n6.5.0` + ), "documentation"); + assert.equal(classifyV6FeedbackType( + "### Feedback type\nDocumentation issue\n\n### Describe the bug\nBug / regression" + ), "documentation"); +}); + +test("a hidden hint or empty topic field is not an actionable declaration", () => { + for (const value of ["", "", "None\n\n"]) { + assert.equal(triage.internals.classifyAdditionalActionableTopics( + `### Additional actionable topics\n${value}\n\n### Pulse version\n6.5.0` + ), false); + } +}); + +test("syncLabels surfaces newly added later-line topics without resetting Community state", async () => { + const previousBody = "### Additional actionable topics\nNone\n\n### Pulse version\n6.5.0"; + const body = "### Additional actionable topics\nNone\n\n" + + "Backup attribution is also wrong.\n\n### Pulse version\n6.5.0"; + for (const action of ["opened", "edited", "reopened"]) { + const { github, calls } = createGithub(); + const issue = { number: 2800, title: "Host inventory report", body, + labels: [{ name: "enhancement" }, { name: "needs-retest-on-latest" }, { name: "needs-human" }] }; + await syncLabels({ github, core: createCore(), + context: createContext({ action, issue, previousBody }) }); + assert.deepEqual(calls.addLabels.flatMap(call => call.labels), + action === "reopened" ? [] : ["needs-decomposition"]); + assert.deepEqual(calls.removeLabel, []); + assert.deepEqual(calls.createComment, []); + assert.deepEqual(calls.getLatestRelease, []); + assert.deepEqual(calls.paginate, []); + assert.deepEqual(calls.getIssue.map(call => call.issue_number), [2800]); + } +}); + +test("syncLabels ignores hidden bug examples and topic hints", async () => { + const { github, calls } = createGithub(); + const issue = { number: 2801, title: "Documentation wording", + body: "\n" + + "### Feedback type\nDocumentation issue\n\n" + + "### Additional actionable topics\nNone\n\n### Pulse version\n6.5.0", + labels: [{ name: "needs-retest-on-latest" }, { name: "needs-decomposition" }] }; + await syncLabels({ github, core: createCore(), context: createContext({ issue }) }); + assert.deepEqual(calls.addLabels.flatMap(call => call.labels), ["documentation"]); + assert.deepEqual(calls.createLabel.map(call => call.name), []); + assert.deepEqual(calls.removeLabel, []); + assert.deepEqual(calls.createComment, []); + assert.deepEqual(calls.getLatestRelease, []); + assert.deepEqual(calls.paginate, []); +}); + test("every actionable issue form exposes the decomposition signal", () => { const templateDir = path.resolve(__dirname, "../ISSUE_TEMPLATE"); for (const name of [ diff --git a/docs/ISSUE_TRIAGE.md b/docs/ISSUE_TRIAGE.md index 3ea90ff049..b660a7c7ba 100644 --- a/docs/ISSUE_TRIAGE.md +++ b/docs/ISSUE_TRIAGE.md @@ -197,6 +197,13 @@ topics or create issues automatically. A maintainer or triage agent reviews the source context, chooses the correct destination, creates links, and is accountable for the disposition. +Classification reads the complete declared field, including later paragraphs +and topic subheadings, not only its first line. An initial "None" followed by an +actionable topic is not an empty declaration. Hidden HTML template comments +cannot supply a field or feedback type, and the next form field's contents are +not secondary topics. These labels surface review work; they do not establish +that a defect is reproduced or a request will be built. + Before changing metadata, the synchronizer reads the current issue body and labels, rather than classifying a queued event's older report. A failed read stops the job without a stale-data fallback; closed issues and pull requests are diff --git a/frontend-modern/public/docs/ISSUE_TRIAGE.md b/frontend-modern/public/docs/ISSUE_TRIAGE.md index 3ea90ff049..b660a7c7ba 100644 --- a/frontend-modern/public/docs/ISSUE_TRIAGE.md +++ b/frontend-modern/public/docs/ISSUE_TRIAGE.md @@ -197,6 +197,13 @@ topics or create issues automatically. A maintainer or triage agent reviews the source context, chooses the correct destination, creates links, and is accountable for the disposition. +Classification reads the complete declared field, including later paragraphs +and topic subheadings, not only its first line. An initial "None" followed by an +actionable topic is not an empty declaration. Hidden HTML template comments +cannot supply a field or feedback type, and the next form field's contents are +not secondary topics. These labels surface review work; they do not establish +that a defect is reproduced or a request will be built. + Before changing metadata, the synchronizer reads the current issue body and labels, rather than classifying a queued event's older report. A failed read stops the job without a stale-data fallback; closed issues and pull requests are