From 505dcd571fcbb0400c121706de6fc5385872dd8f Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 10:15:45 +0100 Subject: [PATCH] Keep server rollback guidance executable --- docs/UPGRADE_v6.md | 4 +-- .../subsystems/deployment-installability.md | 15 +++++++++ docs/releases/RELEASE_NOTES_v6.4.0.md | 2 +- docs/releases/V6_CHANGELOG_v6.4.0.md | 2 +- frontend-modern/public/docs/UPGRADE_v6.md | 4 +-- .../installtests/build_release_assets_test.go | 32 +++++++++++++++++++ .../record_rc_to_ga_rehearsal_test.py | 14 ++++---- .../internal/resolve_release_promotion.py | 2 +- .../resolve_release_promotion_test.py | 4 +-- .../release_note_visuals_test.py | 2 +- .../release_promotion_policy_test.py | 2 +- .../release_control/render_release_body.py | 10 ++++++ .../render_release_body_test.py | 11 +++++-- .../resolve_release_promotion.py | 5 ++- .../resolve_release_promotion_test.py | 8 ++--- scripts/trigger-release.sh | 4 +-- 16 files changed, 93 insertions(+), 28 deletions(-) diff --git a/docs/UPGRADE_v6.md b/docs/UPGRADE_v6.md index 3b603e744..8f69732e5 100644 --- a/docs/UPGRADE_v6.md +++ b/docs/UPGRADE_v6.md @@ -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) diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 13852a248..084904784 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -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 diff --git a/docs/releases/RELEASE_NOTES_v6.4.0.md b/docs/releases/RELEASE_NOTES_v6.4.0.md index 7db1cef85..8da8dcd27 100644 --- a/docs/releases/RELEASE_NOTES_v6.4.0.md +++ b/docs/releases/RELEASE_NOTES_v6.4.0.md @@ -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. diff --git a/docs/releases/V6_CHANGELOG_v6.4.0.md b/docs/releases/V6_CHANGELOG_v6.4.0.md index 68c8f502e..c47511471 100644 --- a/docs/releases/V6_CHANGELOG_v6.4.0.md +++ b/docs/releases/V6_CHANGELOG_v6.4.0.md @@ -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. diff --git a/frontend-modern/public/docs/UPGRADE_v6.md b/frontend-modern/public/docs/UPGRADE_v6.md index 3b603e744..8f69732e5 100644 --- a/frontend-modern/public/docs/UPGRADE_v6.md +++ b/frontend-modern/public/docs/UPGRADE_v6.md @@ -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) diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index b2bd1c8a4..158f004f0 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -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 { diff --git a/scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py b/scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py index 7c3ff7f7b..887ceb126 100644 --- a/scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py +++ b/scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py @@ -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", ] ) diff --git a/scripts/release_control/internal/resolve_release_promotion.py b/scripts/release_control/internal/resolve_release_promotion.py index 5811bfb41..8c1ca5478 100644 --- a/scripts/release_control/internal/resolve_release_promotion.py +++ b/scripts/release_control/internal/resolve_release_promotion.py @@ -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 = "" diff --git a/scripts/release_control/internal/resolve_release_promotion_test.py b/scripts/release_control/internal/resolve_release_promotion_test.py index cabfb9502..d3f602997 100644 --- a/scripts/release_control/internal/resolve_release_promotion_test.py +++ b/scripts/release_control/internal/resolve_release_promotion_test.py @@ -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"], diff --git a/scripts/release_control/release_note_visuals_test.py b/scripts/release_control/release_note_visuals_test.py index 03af382e5..cb169a99a 100644 --- a/scripts/release_control/release_note_visuals_test.py +++ b/scripts/release_control/release_note_visuals_test.py @@ -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", }, )() ) diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index ac627c118..2d6f3fb3e 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -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) diff --git a/scripts/release_control/render_release_body.py b/scripts/release_control/render_release_body.py index 9d8281cd0..419cd539b 100644 --- a/scripts/release_control/render_release_body.py +++ b/scripts/release_control/render_release_body.py @@ -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.", ] ) diff --git a/scripts/release_control/render_release_body_test.py b/scripts/release_control/render_release_body_test.py index e26de02c0..2e1ca7302 100644 --- a/scripts/release_control/render_release_body_test.py +++ b/scripts/release_control/render_release_body_test.py @@ -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( diff --git a/scripts/release_control/resolve_release_promotion.py b/scripts/release_control/resolve_release_promotion.py index 75ca35db9..bc72ff5db 100644 --- a/scripts/release_control/resolve_release_promotion.py +++ b/scripts/release_control/resolve_release_promotion.py @@ -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 = "" diff --git a/scripts/release_control/resolve_release_promotion_test.py b/scripts/release_control/resolve_release_promotion_test.py index cadfd5822..f8517fb40 100644 --- a/scripts/release_control/resolve_release_promotion_test.py +++ b/scripts/release_control/resolve_release_promotion_test.py @@ -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"], diff --git a/scripts/trigger-release.sh b/scripts/trigger-release.sh index c9bf90229..49d35499d 100755 --- a/scripts/trigger-release.sh +++ b/scripts/trigger-release.sh @@ -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 ""