Preserve secondary issue feedback

Contract-Neutral: issue-triage intake only; no product, deployment, or release contract change
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-08-29 12:23:37 +01:00
parent b75a5aeec2
commit 6cb5d3046b
10 changed files with 287 additions and 35 deletions
+9
View File
@@ -41,6 +41,15 @@ body:
validations:
required: true
- type: textarea
id: additional_topics
attributes:
label: Additional actionable topics
description: Enter "None" for a single bug. If this report also raises another bug, feature, or workflow, list each topic here; triage will preserve it with a separate linked disposition.
placeholder: None
validations:
required: true
- type: input
id: pulse_version
attributes:
-30
View File
@@ -1,30 +0,0 @@
---
name: Feature request
about: Describe an operator problem or workflow Pulse could improve
title: "[Feature]: "
labels: enhancement
assignees: ""
---
## Problem
What are you trying to accomplish, and what makes it difficult today?
## Current workflow
Describe how you handle this now. Include the relevant platform, Pulse page,
deployment type, and constraints.
## Desired outcome
What would a successful Pulse workflow let you do? Focus on the outcome rather
than prescribing an implementation when possible.
## Alternatives considered
What workarounds, other tools, or different approaches have you tried?
## Additional context
Add screenshots, examples, or related issues. Do not include credentials,
tokens, private keys, or unsanitized configuration files.
@@ -0,0 +1,65 @@
name: Feature request
description: Describe one operator problem or workflow Pulse could improve
title: "[Feature]: "
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
Lead with one primary operator outcome. If context exposes another actionable topic, list it in the dedicated field below; triage will preserve it rather than silently folding it into the primary request.
Do not include credentials, tokens, private keys, or unsanitized configuration files.
- type: textarea
id: problem
attributes:
label: Problem
description: What are you trying to accomplish, and what makes it difficult today?
validations:
required: true
- type: textarea
id: current_workflow
attributes:
label: Current workflow
description: Describe how you handle this now. Include the relevant platform, Pulse page, deployment type, and constraints.
validations:
required: true
- type: textarea
id: desired_outcome
attributes:
label: Desired outcome
description: What would a successful Pulse workflow let you do? Focus on the outcome rather than prescribing an implementation when possible.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: What workarounds, other tools, or different approaches have you tried?
- type: textarea
id: additional_topics
attributes:
label: Additional actionable topics
description: Enter "None" for a single request. If the context also raises another bug, feature, or workflow, list each topic here; triage will preserve it with a separate linked disposition.
placeholder: None
validations:
required: true
- type: textarea
id: additional_context
attributes:
label: Additional context
description: Add screenshots, examples, or related issues. Do not include credentials, tokens, private keys, or unsanitized configuration files.
- type: checkboxes
id: confirmations
attributes:
label: Confirmations
options:
- label: I searched existing issues for duplicates.
required: true
@@ -123,6 +123,15 @@ body:
validations:
required: true
- type: textarea
id: additional_topics
attributes:
label: Additional actionable topics
description: Enter "None" for a single topic. If this report also raises another bug, feature, or workflow, list each topic here; triage will preserve it with a separate linked disposition.
placeholder: None
validations:
required: true
- type: textarea
id: evidence
attributes:
+49 -1
View File
@@ -1,6 +1,7 @@
const VERSION_LABEL_PREFIX = "affects-";
const NEEDS_VERSION_LABEL = "needs-version-info";
const RETEST_LABEL = "needs-retest-on-latest";
const NEEDS_DECOMPOSITION_LABEL = "needs-decomposition";
const RETEST_COMMENT_MARKER = "<!-- issue-version-triage:v1 -->";
const CLOSE_COMMENT_MARKER = "<!-- issue-timeout-close:v1 -->";
const TRIAGE_FOOTER =
@@ -28,6 +29,28 @@ function extractSectionValue(body, heading) {
return value || null;
}
function classifyAdditionalActionableTopics(body) {
const value = extractSectionValue(body, "Additional actionable topics");
if (value === null) return null;
const normalized = value
.replace(/<!--([\s\S]*?)-->/g, "")
.trim()
.toLowerCase()
.replace(/[.!]+$/g, "");
if (!normalized) return false;
return !new Set([
"_no response_",
"n/a",
"na",
"no",
"none",
"none known",
"not applicable",
]).has(normalized);
}
function normalizeVersion(value) {
if (!value) return null;
const match = String(value).match(/\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/i);
@@ -170,6 +193,14 @@ function buildTriageState(issue, core, latestVersion) {
nextLabels.add(v6FeedbackClass);
}
const hasAdditionalActionableTopics = classifyAdditionalActionableTopics(issue.body);
if (hasAdditionalActionableTopics === true) {
core.info("Issue declares additional actionable topics; decomposition is required.");
nextLabels.add(NEEDS_DECOMPOSITION_LABEL);
} else if (hasAdditionalActionableTopics === false) {
nextLabels.delete(NEEDS_DECOMPOSITION_LABEL);
}
const reportedVersion = extractPulseVersion(issue.title, issue.body);
core.info(`Reported Pulse version: ${reportedVersion || "not found"}`);
core.info(`Latest stable release: ${latestVersion || "unknown"}`);
@@ -179,6 +210,7 @@ function buildTriageState(issue, core, latestVersion) {
nextLabels,
reportedVersion,
v6FeedbackClass,
hasAdditionalActionableTopics,
isBugLike: nextLabels.has(BUG_LABEL),
comparison:
reportedVersion && latestVersion ? compareCore(reportedVersion, latestVersion) : null,
@@ -248,13 +280,27 @@ async function syncLabels({ github, context, core }) {
nextLabels,
reportedVersion,
v6FeedbackClass,
hasAdditionalActionableTopics,
isBugLike,
comparison,
} = buildTriageState(issue, core, latestVersion);
if (hasAdditionalActionableTopics === true) {
await ensureLabel(
github,
context,
NEEDS_DECOMPOSITION_LABEL,
"fbca04",
"Issue declares additional actionable topics that need linked dispositions"
);
}
if (!isBugLike) {
core.info("Issue is not bug-like after classification. Skipping version triage.");
if (v6FeedbackClass && !labelNames.has(v6FeedbackClass)) {
const labelsChanged =
labelNames.size !== nextLabels.size ||
[...labelNames].some((label) => !nextLabels.has(label));
if (labelsChanged) {
await github.rest.issues.setLabels({
owner: context.repo.owner,
repo: context.repo.repo,
@@ -433,6 +479,7 @@ module.exports = {
CLOSE_COMMENT_MARKER,
DOCS_LABEL,
ENHANCEMENT_LABEL,
NEEDS_DECOMPOSITION_LABEL,
NEEDS_VERSION_LABEL,
RETEST_COMMENT_MARKER,
RETEST_COMMENT_GRACE_MS,
@@ -444,6 +491,7 @@ module.exports = {
buildTimeoutCloseCommentBody,
buildTriageState,
canPostRetestComment,
classifyAdditionalActionableTopics,
classifyV6FeedbackType,
compareCore,
extractPulseVersion,
@@ -1,5 +1,7 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const triage = require("./issue-version-triage.cjs");
@@ -124,6 +126,92 @@ test("syncLabels only adds documentation classification for non-bug v6 feedback"
assert.deepEqual(calls.setLabels[0].labels, ["documentation"]);
});
test("syncLabels marks declared secondary topics for decomposition", async () => {
const { github, calls } = createGithub({ latestVersion: "6.4.1" });
const issue = {
number: 1796,
title: "Availability workflow feedback",
body: [
"## Problem",
"Machine availability is hard to scan.",
"",
"## Additional actionable topics",
"The triage bot should preserve secondary requests.",
].join("\n"),
labels: [{ name: "enhancement" }],
};
await triage.syncLabels({
github,
context: createContext({ issue }),
core: createCore(),
});
assert.deepEqual(calls.createLabel.map((call) => call.name), [
"needs-decomposition",
]);
assert.deepEqual(calls.setLabels[0].labels, [
"enhancement",
"needs-decomposition",
]);
});
test("syncLabels clears decomposition after every declared topic has a disposition", async () => {
const { github, calls } = createGithub({ latestVersion: "6.4.1" });
const issue = {
number: 1796,
title: "Availability workflow feedback",
body: "## Additional actionable topics\nNone.\n",
labels: [{ name: "enhancement" }, { name: "needs-decomposition" }],
};
await triage.syncLabels({
github,
context: createContext({ action: "edited", issue }),
core: createCore(),
});
assert.equal(calls.createLabel.length, 0);
assert.deepEqual(calls.setLabels[0].labels, ["enhancement"]);
});
test("additional topic classification is explicit and fail-quiet for legacy forms", () => {
const { classifyAdditionalActionableTopics } = triage.internals;
assert.equal(classifyAdditionalActionableTopics("## Problem\nOne thing\n"), null);
assert.equal(
classifyAdditionalActionableTopics("## Additional actionable topics\n_No response_\n"),
false
);
assert.equal(
classifyAdditionalActionableTopics("## Additional actionable topics\nNone known.\n"),
false
);
assert.equal(
classifyAdditionalActionableTopics(
"## Additional actionable topics\n- Add a storage filter\n- Reduce log noise\n"
),
true
);
});
test("every actionable issue form exposes the decomposition signal", () => {
const templateDir = path.resolve(__dirname, "../ISSUE_TEMPLATE");
for (const name of [
"bug_report.yml",
"feature_request.yml",
"v6_rc_feedback.yml",
]) {
const form = fs.readFileSync(path.join(templateDir, name), "utf8");
assert.match(form, /id: additional_topics/);
assert.match(form, /label: Additional actionable topics/);
assert.match(
form,
/id: additional_topics[\s\S]*?validations:\s*\n\s+required: true/
);
}
});
test("postRetestComment comments once for older non-maintainer bug reports", async () => {
const { github, calls } = createGithub({ latestVersion: "6.0.1" });
const issue = {
+3 -1
View File
@@ -3,14 +3,16 @@
## Issue Triage Automation
**Files**:
- `issue-version-label-sync.yml`
- `issue-version-retest-comment.yml`
Issue intake is split deliberately:
- `issue-version-label-sync.yml` is the silent metadata path. It runs on `opened`, `edited`, and `reopened` issue events so version labels, `needs-version-info`, and `needs-retest-on-latest` stay correct when maintainers tidy issue metadata.
- `issue-version-label-sync.yml` is the silent metadata path. It runs on `opened`, `edited`, and `reopened` issue events so version labels, `needs-version-info`, `needs-retest-on-latest`, and the explicit `needs-decomposition` topic-integrity signal stay correct when maintainers tidy issue metadata.
- `issue-version-retest-comment.yml` is the scheduled public guidance path. It gives maintainers a grace window, then posts reporter-facing retest guidance only when an older-version bug report from a non-maintainer has no existing maintainer response. This prevents generic stable-version advice from contradicting a specific maintainer fix or prerelease boundary.
- Both workflows load the shared helper at `.github/scripts/issue-version-triage.cjs` so parsing and classification logic lives in one place instead of drifting across duplicated inline scripts.
- `needs-decomposition` is driven only by the structured **Additional actionable topics** form field. The [triage contract](../../docs/ISSUE_TRIAGE.md) requires human or agent judgment to create linked dispositions; the workflow does not infer or auto-create issues from free text.
## Update Demo Server