Integrate reviewed failed-upgrade intake guidance

Accept exact candidate 6809ca2b23. Keep attempted upgrades, failing runtimes and later recovery distinct while preserving passive, private evidence collection. No runtime or release outcome is asserted.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-10 12:12:06 +01:00
6 changed files with 195 additions and 6 deletions
+19 -3
View File
@@ -64,7 +64,7 @@ body:
id: pulse_version
attributes:
label: Pulse version
description: Running version for an existing install; for a failed install, the exact version or release asset attempted, or "unknown" if it was not shown.
description: Version running when the problem occurred, or "unknown" if unavailable. For a failed install, give the exact version or release asset attempted. Keep an interrupted upgrade's target and later recovered version separate in Upgrade attempt and recovery below.
placeholder: v6.x.y, release asset name, or unknown
validations:
required: true
@@ -124,11 +124,27 @@ body:
id: installer_source
attributes:
label: Installer or helper source
description: For a failed install, give the public installer/helper URL or command name, if known. Do not paste a command containing a token or other secret.
description: For a failed install or upgrade, give the public installer/helper URL or command name, if known. Do not paste a command containing a token or other secret.
placeholder: Official Pulse installer, helper URL, or command name
validations:
required: false
- type: textarea
id: upgrade_context
attributes:
label: Upgrade attempt and recovery
description: |
For an interrupted or failed upgrade only: give the starting version, intended version or asset, route (in-app, automatic timer, manual installer or helper), original time and timezone, and last recorded updater/installer result or exit code. Distinguish Pulse stopping from the whole LXC, VM or host stopping. Say what recovery you already performed and which version and state you observed afterwards.
A verified download signature is not a completed upgrade; a later boot does not explain the stop. Today's running version does not establish the version at failure. Use existing observations; blank or "unknown" is valid. If already available, include only relevant, locally reviewed and redacted updater and host stop records in the evidence field, not full journals.
Do not rerun the update, reboot, restore, run Diagnostics or delete rollback backups just to complete this report. Keep backups private and preserve the original evidence.
placeholder: |
Attempt: starting version → intended version; route; time and timezone
Last recorded result: error or exit code, or unknown
Observed stop: Pulse service / whole LXC, VM or host / unknown
Recovery already performed: none or original steps; version/state afterwards or unknown
validations:
required: false
- type: textarea
id: logs
attributes:
@@ -154,7 +170,7 @@ body:
attributes:
label: Confirmations
options:
- label: I gave the running Pulse version, or for an install that never started, the attempted version or "unknown"; for a running container I also gave its image tag or digest.
- label: I gave the Pulse version at the failure or "unknown" if unavailable; for an install that never started, the attempted version or "unknown"; for a running container I also gave its image tag or digest.
required: true
- label: I searched existing issues for duplicates.
required: true
+19 -3
View File
@@ -34,7 +34,7 @@ body:
id: pulse_version
attributes:
label: Pulse version
description: Running version for an existing install; for a failed install, the exact version or release asset attempted, or "unknown" if it was not shown.
description: Version running when the problem occurred, or "unknown" if unavailable. For a failed install, give the exact version or release asset attempted. Keep an interrupted upgrade's target and later recovered version separate in Upgrade attempt and recovery below.
placeholder: v6.x.y-rc.N, release asset name, or unknown
validations:
required: true
@@ -97,11 +97,27 @@ body:
id: installer_source
attributes:
label: Installer or helper source
description: For a failed install, give the public installer/helper URL or command name, if known. Do not paste a command containing a token or other secret.
description: For a failed install or upgrade, give the public installer/helper URL or command name, if known. Do not paste a command containing a token or other secret.
placeholder: Official Pulse installer, helper URL, or command name
validations:
required: false
- type: textarea
id: upgrade_context
attributes:
label: Upgrade attempt and recovery
description: |
For an interrupted or failed upgrade only: give the starting version, intended version or asset, route (in-app, automatic timer, manual installer or helper), original time and timezone, and last recorded updater/installer result or exit code. Distinguish Pulse stopping from the whole LXC, VM or host stopping. Say what recovery you already performed and which version and state you observed afterwards.
A verified download signature is not a completed upgrade; a later boot does not explain the stop. Today's running version does not establish the version at failure. Use existing observations; blank or "unknown" is valid. If already available, include only relevant, locally reviewed and redacted updater and host stop records in the evidence field, not full journals.
Do not rerun the update, reboot, restore, run Diagnostics or delete rollback backups just to complete this report. Keep backups private and preserve the original evidence.
placeholder: |
Attempt: starting version → intended version; route; time and timezone
Last recorded result: error or exit code, or unknown
Observed stop: Pulse service / whole LXC, VM or host / unknown
Recovery already performed: none or original steps; version/state afterwards or unknown
validations:
required: false
- type: input
id: environment
attributes:
@@ -194,7 +210,7 @@ body:
attributes:
label: Confirmations
options:
- label: I gave the running Pulse version, or for an install that never started, the attempted version or "unknown"; for a running container I also gave its Pulse image tag or digest.
- label: I gave the Pulse version at the failure or "unknown" if unavailable; for an install that never started, the attempted version or "unknown"; for a running container I also gave its Pulse image tag or digest.
required: true
- label: I searched existing issues and the pinned v6 pre-release feedback hub first.
required: true
@@ -500,6 +500,32 @@ test("bug and pre-release forms accept truthful evidence for installs that never
}
});
test("both bug forms keep an optional failed-upgrade timeline without unsafe collection", () => {
for (const name of ["bug_report.yml", "v6_rc_feedback.yml"]) {
const form = fs.readFileSync(path.resolve(__dirname, "../ISSUE_TEMPLATE", name), "utf8");
const timeline = form.split(" id: upgrade_context\n")[1]?.split(" - type: ")[0];
assert.ok(timeline, name);
assert.match(timeline, /label: Upgrade attempt and recovery/);
assert.match(timeline, /required: false/);
for (const distinction of [
"starting version, intended version or asset", "original time and timezone",
"last recorded updater/installer result or exit code",
"whole LXC, VM or host stopping", "recovery you already performed",
"A verified download signature is not a completed upgrade",
"a later boot does not explain the stop",
"Today's running version does not establish the version at failure",
'blank or "unknown" is valid', "not full journals",
"Do not rerun the update, reboot, restore, run Diagnostics or delete rollback backups",
"Keep backups private and preserve the original evidence",
]) {
assert.ok(timeline.includes(distinction), `${name}: ${distinction}`);
}
const version = form.split(" id: pulse_version\n")[1].split(" - type: ")[0];
assert.match(version, /Version running when the problem occurred, or "unknown"/);
assert.match(version, /target and later recovered version separate/);
}
});
test("bug and pre-release forms accept unsafe one-off failures without a second run", () => {
const templateDir = path.resolve(__dirname, "../ISSUE_TEMPLATE");
for (const name of ["bug_report.yml", "v6_rc_feedback.yml"]) {
@@ -941,6 +967,42 @@ test("stable-to-preview intake keeps baseline, agent and platform versions out o
}
});
test("interrupted-upgrade targets and recovered versions cannot supply the failing version", async () => {
for (const [failing, expected] of [
["6.4.5", "affects-6.4.5"],
["unknown", "needs-version-info"],
]) {
for (const formTitle of ["[Bug]:", "[v6 pre-release]:"]) {
const { github, calls } = createGithub({ latestVersion: "6.5.0" });
const bugForm = formTitle === "[Bug]:";
const issue = {
number: 2785,
title: `${formTitle} interrupted upgrade to 6.5.0`,
body: [
...(!bugForm ? ["### Feedback type", "Bug / regression"] : []),
"### Pulse version", failing,
"### Last known working Pulse version", "6.4.1",
"### Agent version", "6.5.0",
"### Upgrade attempt and recovery",
"Attempt: 6.4.5 → 6.5.0; automatic timer; 6 October 04:20 UTC",
"Last recorded result: signature verified, then Broken pipe; exit unknown",
"Observed stop: whole LXC; stop time/cause unknown",
"Recovery already performed: booted 7 October; current version 6.5.0",
"### Additional actionable topics", "None",
].join("\n\n"),
// GitHub applies the bug form's label; the preview form declares its
// classification in Feedback type instead. Reproduce both inputs.
labels: bugForm ? [{ name: "bug" }] : [],
};
await syncLabels({ github, context: createContext({ issue }), core: createCore() });
assert.deepEqual(calls.addLabels.flatMap((call) => call.labels).sort(),
(bugForm ? [expected] : [expected, "bug"]).sort());
assert.equal(calls.createComment.length, 0);
assert.equal(calls.getLatestRelease.length, 0);
}
}
});
test("delayed events classify the current report and labels, not an old version", async () => {
const eventIssue = {
number: 2800,
+18
View File
@@ -41,6 +41,24 @@ An API-only connection can have no agent. Accept "unknown" or an omitted optiona
field, and use existing evidence; do not ask for a downgrade, restart, reinstall
or re-enrolment to obtain version context. Existing reports need no refile.
An interrupted upgrade needs a timeline, not one inferred version. The optional
**Upgrade attempt and recovery** field separates the starting version, intended
version or asset, route, original time and timezone, last recorded updater or
installer result, what stopped, and recovery already performed. Keep the version
running at failure separate from the version observed after recovery; accept
unknowns and use evidence already supplied anywhere in the thread.
A verified download signature establishes artifact authenticity, not completed
installation, successful rollback or a healthy running service. `Broken pipe`
alone does not establish why a service or its host stopped. A later boot marks
a new run, not the stop's time or cause. Distinguish Pulse stopping from its
whole LXC, VM or host stopping, using an existing host stop record only when
available and consequential. Ask for only the missing distinction, not full
journals or another version already supplied. Do not rerun the update, reboot,
restore, run Diagnostics or delete rollback backups just to complete a report;
keep backups private and preserve the original evidence. Version labels are
intake metadata, not proof of the failing executable or installed recovery.
The optional **OS / environment** field in both bug forms also distinguishes
where readings come from: the platform API, a Pulse agent, or both. Retain the
affected platform release, including TrueNAS SCALE versus CORE, separately from
@@ -41,6 +41,24 @@ An API-only connection can have no agent. Accept "unknown" or an omitted optiona
field, and use existing evidence; do not ask for a downgrade, restart, reinstall
or re-enrolment to obtain version context. Existing reports need no refile.
An interrupted upgrade needs a timeline, not one inferred version. The optional
**Upgrade attempt and recovery** field separates the starting version, intended
version or asset, route, original time and timezone, last recorded updater or
installer result, what stopped, and recovery already performed. Keep the version
running at failure separate from the version observed after recovery; accept
unknowns and use evidence already supplied anywhere in the thread.
A verified download signature establishes artifact authenticity, not completed
installation, successful rollback or a healthy running service. `Broken pipe`
alone does not establish why a service or its host stopped. A later boot marks
a new run, not the stop's time or cause. Distinguish Pulse stopping from its
whole LXC, VM or host stopping, using an existing host stop record only when
available and consequential. Ask for only the missing distinction, not full
journals or another version already supplied. Do not rerun the update, reboot,
restore, run Diagnostics or delete rollback backups just to complete a report;
keep backups private and preserve the original evidence. Version labels are
intake metadata, not proof of the failing executable or installed recovery.
The optional **OS / environment** field in both bug forms also distinguishes
where readings come from: the platform API, a Pulse agent, or both. Retain the
affected platform release, including TrueNAS SCALE versus CORE, separately from
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Guard passive failed-upgrade intake, not installer or workload recovery."""
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[2]
class FailedUpgradeIntakeDocsTest(unittest.TestCase):
def guidance(self):
text = (ROOT / "docs/ISSUE_TRIAGE.md").read_text()
start = "An interrupted upgrade needs a timeline, not one inferred version."
self.assertEqual(text.count(start), 1)
return " ".join(text.split(start, 1)[1].split("\nThe optional", 1)[0].split())
def test_shipped_triage_guide_matches_source(self):
self.assertEqual((ROOT / "docs/ISSUE_TRIAGE.md").read_bytes(),
(ROOT / "frontend-modern/public/docs/ISSUE_TRIAGE.md").read_bytes())
def test_attempt_failure_and_recovery_are_separate_observations(self):
guide = self.guidance()
for distinction in (
"starting version, intended version or asset, route, original time and timezone",
"last recorded updater or installer result, what stopped",
"version running at failure separate from the version observed after recovery",
"accept unknowns and use evidence already supplied anywhere in the thread",
"Version labels are intake metadata, not proof of the failing executable",
):
with self.subTest(distinction=distinction):
self.assertIn(distinction, guide)
def test_signature_and_later_boot_do_not_establish_completion_or_stop_cause(self):
guide = self.guidance()
for limit in (
"artifact authenticity, not completed installation, successful rollback",
"`Broken pipe` alone does not establish why a service or its host stopped",
"A later boot marks a new run, not the stop's time or cause",
"Distinguish Pulse stopping from its whole LXC, VM or host stopping",
):
with self.subTest(limit=limit):
self.assertIn(limit, guide)
def test_collection_does_not_replay_incident_or_destroy_recovery_evidence(self):
guide = self.guidance()
for boundary in (
"existing host stop record only when available and consequential",
"Ask for only the missing distinction, not full journals",
"another version already supplied",
"Do not rerun the update, reboot, restore, run Diagnostics or delete rollback backups",
"keep backups private and preserve the original evidence",
):
with self.subTest(boundary=boundary):
self.assertIn(boundary, guide)
if __name__ == "__main__":
unittest.main()