mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-07 21:31:53 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user