Keep server rollback guidance executable

This commit is contained in:
pulse-triage[bot]
2026-08-29 10:15:45 +01:00
parent bf3d11729b
commit 505dcd571f
16 changed files with 93 additions and 28 deletions
+2 -2
View File
@@ -260,7 +260,7 @@ Yes. Keep a rollback path available while you evaluate v6. The final release
on the v5 line is 5.1.36, so the stable rollback command is:
```bash
./scripts/install.sh --version v5.1.36
sudo /bin/update --version v5.1.36
```
### Why did my v5 install upgrade itself to v6?
@@ -273,7 +273,7 @@ your data and configuration carry over; run through the Post-Upgrade
Checklist above to confirm everything still works. To return to v5, run:
```bash
./scripts/install.sh --version v5.1.36
sudo /bin/update --version v5.1.36
```
## Migration Notes (v6)
@@ -100,6 +100,16 @@ upgrade must recover those values from both split and `--key=value` service
arguments and reproduce them in the generated systemd command without
discarding existing explicit disk exclusions.
Published exact-version install and rollback guidance must preserve the server
and Unified Agent installer boundary. Supported systemd and Proxmox LXC
deployments use the signed `/bin/update --version vX.Y.Z` server helper;
`/opt/pulse/scripts/install.sh` and release archives' `scripts/install.sh` are
Unified Agent installers and must never be presented as server rollback
commands. Docker guidance instead pins the target image and recreates the
container. The promotion resolver, manual release trigger, rendered release
body, current release packet, and shipped upgrade guide must agree on those
deployment-specific paths.
The accelerated exact-SHA release worker must preserve release-gate fidelity
under its own resource envelope. Bounded frontend static checks and integration
image preparation may overlap, but the full frontend test suite and the
@@ -1740,6 +1750,11 @@ artifact-selection behaviour.
`frontend-modern/public/docs/UPGRADE_v6.md` byte-synchronized. The shipped
copy is part of every release packet, and `docsLinks.test.ts` must fail when
release preparation changes only one side.
16. Keep generated install and rollback instructions deployment-specific.
`scripts/installtests/build_release_assets_test.go` must reject a release
trigger, promotion resolver, rendered release body, current upgrade guide,
or current release packet that routes systemd/LXC rollback through the
Unified Agent installer, and must retain explicit Docker image guidance.
## Current State
+1 -1
View File
@@ -21,4 +21,4 @@
- Existing configurations remain valid. Alert identity and history migrations run automatically, with no manual data migration required.
- Pulse Mobile iOS build 12 and Android versionCode 9 remain compatible. The new `alert_fired` push uses the existing `view_alert` action, so no companion mobile release is required.
- Windows Unified Agent binaries are not Authenticode-signed while SignPath remains unavailable and may show an Unknown Publisher warning. Verify downloads with the published checksums and detached signatures.
- The rollback target is stable `v6.3.2`. Use `./scripts/install.sh --version v6.3.2` if you need to return to the previous stable release.
- The rollback target is stable `v6.3.2`. On systemd and Proxmox LXC installs, use `sudo /bin/update --version v6.3.2` if you need to return to the previous stable release. For Docker Compose, pin the image to `rcourtman/pulse:6.3.2` and recreate the container.
+1 -1
View File
@@ -60,7 +60,7 @@ This changelog describes the complete stable release train since `v6.3.2`.
- Promoted prerelease: `v6.4.0-rc.12`
- Runtime content cutoff: `18b22d1ebbfe542484652e419320fc7643a792f0`
- Rollback target: `v6.3.2`
- Rollback command: `./scripts/install.sh --version v6.3.2`
- Rollback command: `sudo /bin/update --version v6.3.2`
- Promotion path: owner-approved expedited exact-SHA stable cutoff from `main`
- Promotion decision: the release owner accepted a shortened soak to deliver bounded fixes for active monitoring correctness and collection-load harm. This is version-bound risk acceptance, not soak evidence or a standing exception.
- Windows signing decision: the standing SignPath-unavailable policy applies. Windows agents are not Authenticode-signed, may show an Unknown Publisher warning, and retain exact-SHA checksums, detached signatures, immutable-manifest verification, and published-digest verification.
+2 -2
View File
@@ -260,7 +260,7 @@ Yes. Keep a rollback path available while you evaluate v6. The final release
on the v5 line is 5.1.36, so the stable rollback command is:
```bash
./scripts/install.sh --version v5.1.36
sudo /bin/update --version v5.1.36
```
### Why did my v5 install upgrade itself to v6?
@@ -273,7 +273,7 @@ your data and configuration carry over; run through the Post-Upgrade
Checklist above to confirm everything still works. To return to v5, run:
```bash
./scripts/install.sh --version v5.1.36
sudo /bin/update --version v5.1.36
```
## Migration Notes (v6)
@@ -3270,6 +3270,38 @@ func TestReleaseTriggersReevaluateVisualsInsteadOfTrustingSidecars(t *testing.T)
}
}
func TestReleaseRollbackGuidanceUsesServerUpdateHelper(t *testing.T) {
for _, path := range []string{
repoFile("scripts", "trigger-release.sh"),
repoFile("scripts", "release_control", "resolve_release_promotion.py"),
repoFile("scripts", "release_control", "render_release_body.py"),
repoFile("docs", "UPGRADE_v6.md"),
repoFile("frontend-modern", "public", "docs", "UPGRADE_v6.md"),
repoFile("docs", "releases", "RELEASE_NOTES_v6.4.0.md"),
repoFile("docs", "releases", "V6_CHANGELOG_v6.4.0.md"),
} {
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
text := string(content)
if !strings.Contains(text, "/bin/update --version") {
t.Fatalf("%s must route systemd/LXC exact-version changes through the server update helper", path)
}
if strings.Contains(text, "./scripts/install.sh --version") {
t.Fatalf("%s routes server rollback through the Unified Agent installer", path)
}
}
renderer, err := os.ReadFile(repoFile("scripts", "release_control", "render_release_body.py"))
if err != nil {
t.Fatalf("read release body renderer: %v", err)
}
if !strings.Contains(string(renderer), "For Docker Compose, set the Pulse image to the rollback target") {
t.Fatal("release body renderer must retain deployment-specific Docker rollback guidance")
}
}
func TestCommittedReleaseVisualSidecarsCarrySelectionEvidence(t *testing.T) {
paths, err := filepath.Glob(repoFile("docs", "releases", "*.visuals.json"))
if err != nil {
@@ -24,7 +24,7 @@ SUMMARY = """# Prerelease-to-GA Rehearsal Summary
- Promotion channel: stable
- Promoted prerelease tag: v6.0.0-rc.1
- Rollback target: v5.1.23
- Rollback command: `./scripts/install.sh --version v5.1.23`
- Rollback command: `sudo /bin/update --version v5.1.23`
- Prerelease soak hours at rehearsal time: 96
- Planned GA date: 2026-03-15
- Planned v5 end-of-support date: 2026-06-11
@@ -43,7 +43,7 @@ class RecordRcToGaRehearsalTest(unittest.TestCase):
self.assertEqual(parsed["tag"], "v6.0.0")
self.assertEqual(parsed["channel_under_rehearsal"], "stable")
self.assertEqual(parsed["promoted_from_rc"], "v6.0.0-rc.1")
self.assertEqual(parsed["rollback_command"], "`./scripts/install.sh --version v5.1.23`")
self.assertEqual(parsed["rollback_command"], "`sudo /bin/update --version v5.1.23`")
self.assertEqual(parsed["planned_ga_date"], "2026-03-15")
self.assertEqual(parsed["planned_v5_eos_date"], "2026-06-11")
self.assertEqual(parsed["windows_authenticode_required"], "false")
@@ -53,7 +53,7 @@ class RecordRcToGaRehearsalTest(unittest.TestCase):
rendered = mod.render_record(
record_date="2026-03-12",
result="pass",
rollback_command="./scripts/install.sh --version v5.1.23",
rollback_command="sudo /bin/update --version v5.1.23",
run_metadata={"headSha": "abc123", "url": "https://github.com/rcourtman/Pulse/actions/runs/12345"},
summary_metadata=mod.parse_summary_markdown(SUMMARY),
summary_source="/tmp/rc-to-ga-rehearsal-summary.md",
@@ -67,7 +67,7 @@ class RecordRcToGaRehearsalTest(unittest.TestCase):
self.assertIn("Candidate stable tag: v6.0.0", rendered)
self.assertIn("Promotion channel: stable", rendered)
self.assertIn("Promoted prerelease tag: v6.0.0-rc.1", rendered)
self.assertIn("Exact rollback or reinstall command: `./scripts/install.sh --version v5.1.23`", rendered)
self.assertIn("Exact rollback or reinstall command: `sudo /bin/update --version v5.1.23`", rendered)
self.assertIn("Exact GA date to publish: 2026-03-15", rendered)
self.assertIn("Exact v5 end-of-support date to publish: 2026-06-13", rendered)
self.assertIn("Windows Authenticode required: false", rendered)
@@ -98,7 +98,7 @@ class RecordRcToGaRehearsalTest(unittest.TestCase):
self.assertIn("v6.0.0-rc.1", content)
self.assertIn("Promotion channel: stable", content)
self.assertIn("2026-03-15", content)
self.assertIn("Exact rollback or reinstall command: `./scripts/install.sh --version v5.1.23`", content)
self.assertIn("Exact rollback or reinstall command: `sudo /bin/update --version v5.1.23`", content)
def test_main_defaults_output_to_canonical_record_path(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
@@ -231,7 +231,7 @@ class RecordRcToGaRehearsalTest(unittest.TestCase):
def test_main_requires_rollback_command_when_summary_omits_it(self) -> None:
summary_without_command = SUMMARY.replace(
"- Rollback command: `./scripts/install.sh --version v5.1.23`\n", ""
"- Rollback command: `sudo /bin/update --version v5.1.23`\n", ""
)
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
@@ -306,7 +306,7 @@ class RecordRcToGaRehearsalTest(unittest.TestCase):
"--v5-eos-date",
"2026-06-11",
"--rollback-command",
"./scripts/install.sh --version v5.1.23",
"sudo /bin/update --version v5.1.23",
]
)
@@ -120,7 +120,7 @@ def resolve_metadata(
)
if not tag_exists_fn(rollback_tag):
raise ValueError(f"rollback_version {rollback_tag} does not exist as a repository tag.")
rollback_command = f"./scripts/install.sh --version {rollback_tag}"
rollback_command = f"sudo /bin/update --version {rollback_tag}"
promoted_from_tag = ""
soak_hours = ""
@@ -38,7 +38,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
tag_exists_fn=lambda tag: tag == "v5.1.14",
)
self.assertEqual(metadata["rollback_tag"], "v5.1.14")
self.assertEqual(metadata["rollback_command"], "./scripts/install.sh --version v5.1.14")
self.assertEqual(metadata["rollback_command"], "sudo /bin/update --version v5.1.14")
self.assertEqual(metadata["promoted_from_tag"], "")
self.assertEqual(metadata["soak_hours"], "")
@@ -97,7 +97,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
self.assertEqual(metadata["promoted_from_tag"], "")
self.assertEqual(metadata["rollback_tag"], "v6.0.0")
self.assertEqual(metadata["rollback_command"], "./scripts/install.sh --version v6.0.0")
self.assertEqual(metadata["rollback_command"], "sudo /bin/update --version v6.0.0")
self.assertEqual(metadata["hotfix_exception"], "true")
self.assertEqual(
metadata["hotfix_reason"],
@@ -177,7 +177,7 @@ class ReleaseNoteVisualPlanTest(unittest.TestCase):
(),
{
"rollback_target": "v6.3.2",
"rollback_command": "./scripts/install.sh --version v6.3.2",
"rollback_command": "sudo /bin/update --version v6.3.2",
},
)()
)
@@ -1667,7 +1667,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertNotIn("secrets.", docker_build)
self.assertNotIn("provenance: false", content)
self.assertIn("Derived rollback command:", helper)
self.assertIn("./scripts/install.sh --version", helper)
self.assertIn("sudo /bin/update --version", helper)
self.assertIn("v6 GA date to publish with GA", helper)
self.assertIn("--arg ga_date \"$GA_DATE\"", helper)
self.assertIn("ga_date", resolver)
@@ -486,6 +486,14 @@ def build_installation_section(version: str) -> str:
[
"## Install",
"",
"For systemd and Proxmox LXC installs, use **Settings → System → Updates** or:",
"",
"```bash",
f"sudo /bin/update --version v{version}",
"```",
"",
"For Docker:",
"",
"```bash",
f"docker pull rcourtman/pulse:{version}",
"```",
@@ -509,6 +517,8 @@ def build_rollback_section(args: argparse.Namespace) -> str:
"```bash",
args.rollback_command,
"```",
"",
"For Docker Compose, set the Pulse image to the rollback target and recreate the container.",
]
)
@@ -499,7 +499,7 @@ Old metadata section.
"candidate_tag": "v6.0.0-rc.2",
"promoted_prerelease_tag": "",
"rollback_target": "v5.1.28",
"rollback_command": "./scripts/install.sh --version v5.1.28",
"rollback_command": "sudo /bin/update --version v5.1.28",
"planned_ga_date": "",
"planned_v5_eos_date": "",
"hotfix_exception": "false",
@@ -523,10 +523,15 @@ Old metadata section.
self.assertEqual(body.count("## Install"), 1)
self.assertEqual(body.count("## Roll back"), 1)
self.assertNotIn("## Promotion Metadata", body)
self.assertIn("sudo /bin/update --version v6.0.0-rc.2", body)
self.assertIn("docker pull rcourtman/pulse:6.0.0-rc.2", body)
self.assertIn("https://pulserelay.pro/download.html", body)
self.assertIn("The rollback target is `v5.1.28`", body)
self.assertIn("./scripts/install.sh --version v5.1.28", body)
self.assertIn("sudo /bin/update --version v5.1.28", body)
self.assertIn(
"For Docker Compose, set the Pulse image to the rollback target",
body,
)
render_release_body.validate_release_body_shape(body, "6.0.0-rc.2")
def test_release_body_accepts_visual_evidence_before_installation(self) -> None:
@@ -553,7 +558,7 @@ Controls remain readable without horizontal scrolling.
(),
{
"rollback_target": "v6.3.2",
"rollback_command": "./scripts/install.sh --version v6.3.2",
"rollback_command": "sudo /bin/update --version v6.3.2",
},
)()
body = "\n\n".join(
@@ -360,7 +360,10 @@ def resolve_metadata(
)
if not tag_exists_fn(rollback_tag):
raise ValueError(f"rollback_version {rollback_tag} does not exist as a repository tag.")
rollback_command = f"./scripts/install.sh --version {rollback_tag}"
# Supported systemd and Proxmox LXC installs expose the signed server
# installer through /bin/update. The archive's scripts/install.sh is the
# Unified Agent installer and deliberately does not accept --version.
rollback_command = f"sudo /bin/update --version {rollback_tag}"
promoted_from_tag = ""
soak_hours = ""
@@ -41,7 +41,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
tag_exists_fn=lambda tag: tag == "v5.1.14",
)
self.assertEqual(metadata["rollback_tag"], "v5.1.14")
self.assertEqual(metadata["rollback_command"], "./scripts/install.sh --version v5.1.14")
self.assertEqual(metadata["rollback_command"], "sudo /bin/update --version v5.1.14")
self.assertEqual(metadata["promoted_from_tag"], "")
self.assertEqual(metadata["soak_hours"], "")
@@ -158,7 +158,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
tag_exists_fn=lambda tag: True,
)
self.assertEqual(metadata["rollback_tag"], "v6.0.4")
self.assertEqual(metadata["rollback_command"], "./scripts/install.sh --version v6.0.4")
self.assertEqual(metadata["rollback_command"], "sudo /bin/update --version v6.0.4")
def test_explicit_rollback_input_wins_over_derivation(self) -> None:
metadata = resolver.resolve_metadata(
@@ -572,7 +572,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
self.assertEqual(metadata["promoted_from_tag"], "v6.0.0-rc.7")
self.assertEqual(metadata["rollback_tag"], "v5.1.35")
self.assertEqual(metadata["rollback_command"], "./scripts/install.sh --version v5.1.35")
self.assertEqual(metadata["rollback_command"], "sudo /bin/update --version v5.1.35")
self.assertEqual(metadata["ga_date"], "2026-07-04")
self.assertEqual(metadata["v5_eos_date"], "2026-10-02")
@@ -596,7 +596,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
self.assertEqual(metadata["promoted_from_tag"], "")
self.assertEqual(metadata["rollback_tag"], "v6.0.1")
self.assertEqual(metadata["rollback_command"], "./scripts/install.sh --version v6.0.1")
self.assertEqual(metadata["rollback_command"], "sudo /bin/update --version v6.0.1")
self.assertEqual(metadata["hotfix_exception"], "true")
self.assertEqual(
metadata["hotfix_reason"],
+2 -2
View File
@@ -242,9 +242,9 @@ if [ -z "$ROLLBACK_VERSION" ]; then
fi
if [[ "$ROLLBACK_VERSION" == v* ]]; then
ROLLBACK_COMMAND="./scripts/install.sh --version ${ROLLBACK_VERSION}"
ROLLBACK_COMMAND="sudo /bin/update --version ${ROLLBACK_VERSION}"
else
ROLLBACK_COMMAND="./scripts/install.sh --version v${ROLLBACK_VERSION}"
ROLLBACK_COMMAND="sudo /bin/update --version v${ROLLBACK_VERSION}"
fi
echo ""