Qualify rollback help by updater and backup scope

Separate installation rollback from stopped full-state recovery, correct the install guide's automatic-backup promise and name the verified limited published snapshots. Keep shipped help matched, reject the original unsafe claims and exercise desktop and phone Docs links without claiming the unready backend has shipped.

Contract-Neutral: Documentation and help verification only; no updater runtime or API contract changes.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-03 07:02:30 +01:00
parent 0b185f9392
commit 45835634ea
6 changed files with 151 additions and 28 deletions
+24 -5
View File
@@ -128,8 +128,9 @@ another update just to reproduce a failure.
Before any rollback, take a consistent private backup of the current state and
check the proposed backup's scope. Keep the failed installation and update
snapshots until recovery is verified. A rollback can revert settings, alert
changes and history written since the selected backup.
snapshots until recovery is verified. Returning to an older binary is not a
full-state recovery. Restoring a matching data snapshot can discard settings,
alert changes and history written since that snapshot.
### Automatic Rollback
@@ -144,8 +145,16 @@ health afterwards; a progress message alone is not recovery evidence.
If your installed version offers it, open **Settings → System → Updates →
Update History**. **Roll back** is offered for a successful in-app update with
a retained backup. The confirmation names the version before that update and
warns that later settings and alert changes will be reverted. Check that version
and backup before confirming; Pulse restarts to use the restored binary.
Pulse restarts to use the restored binary. Check that version and the actual
backup contents before confirming.
**Update History is not a full-state recovery tool.** Its restore scope depends
on the installed updater. Older updaters may replace `data/`, `config/` and
`.env` under the installation directory, but omit active data stored elsewhere.
Do not assume later settings and alert changes will be reverted, even if the
confirmation says so. If active data shares those installation paths, do not
restore it while Pulse is running; use the stopped-service procedure in
[Manual Rollback](#manual-rollback).
Older versions may not have this control. An absent button or backup does not
justify deleting data or creating an empty configuration. Docker server installs
@@ -159,13 +168,23 @@ low-space fallback. Use the `backup_path` recorded for the actual update in
`update-history.jsonl`, rather than guessing a timestamp. The updater retains
the most recent three snapshots; a `/tmp` snapshot may also be lost on reboot.
The in-app snapshot copies the running server binary, available `VERSION` and
Snapshot scope is version-specific. The published **v6.4.5** and
**v6.4.6-rc.1** in-app updaters use the limited installation snapshot described
below. Do not assume a backup made by another updater version has the same
contents or restore behaviour.
The limited snapshot copies the running server binary, available `VERSION` and
`.env` files, and available `data/` and `config/` directories under
`PULSE_INSTALL_DIR` (default `/opt/pulse`). It is not necessarily a complete
backup of the active `PULSE_DATA_DIR`, external metrics/audit stores, service
units, bundled agents or update helpers. Copy errors can leave a partial
snapshot: a recorded path is not proof that every file was saved.
A backup's existence, or the presence of a database file, does not establish
completeness, consistency or that Update History will restore it. A snapshot
that includes active data still needs a verified, stopped-service recovery
procedure; its presence is not permission to rewind live runtime stores.
Verify the backup against your effective service paths and update logs. Preserve
the active data directory, matching `.encryption.key`, audit history and its
signing key together, including custom paths and organisation directories. Use
+14 -2
View File
@@ -302,9 +302,21 @@ update. Use **Settings → Infrastructure → Install on a host** for a first in
or a v5-to-v6 in-place upgrade.
### Rollback
If an update causes issues on systemd installations, backups are created automatically during the update process.
An update error does not establish which version is running. Check the running
version and service health before retrying or rolling back, and preserve the
failed installation and update logs.
**Manual rollback**: In-app updates store backups under `/etc/pulse/backup-<timestamp>/`. The systemd auto-update timer uses a temporary `/tmp/pulse-backup-<timestamp>` during the update and auto-restores on failure.
In-app update snapshots and updater-script backups have different contents and
lifetimes. Neither is guaranteed to include all active data; an older binary
also may not understand data migrated by a newer version. Update History is
not a full-state recovery tool, and a recorded backup path is not proof of a
complete backup.
Check the [version-specific snapshot scope](AUTO_UPDATE.md#what-an-update-snapshot-contains)
and use the [stopped-service recovery procedure](AUTO_UPDATE.md#manual-rollback)
when data needs restoring. Keep matching data and keys together, verify the
backup privately and preserve reversible copies of the failed state. Do not
replace live runtime data or delete it to make a rollback fit.
---
@@ -48,7 +48,10 @@ const { chromium, webkit } = require('playwright');
for (const text of ['Roll back from Update History', 'successful in-app update',
'not necessarily a complete backup', 'Copy errors can leave a partial snapshot',
'matching .encryption.key', 'SQLite sidecar files', 'keep reversible copies',
'An image change alone is not a data rollback']) assert.ok(article.includes(text), text);
'An image change alone is not a data rollback', 'published v6.4.5 and v6.4.6-rc.1',
'Update History is not a full-state recovery tool',
'Do not assume later settings and alert changes will be reverted',
'not permission to rewind live runtime stores']) assert.ok(article.includes(text), text);
assert.ok(!article.includes('There is no rollback UI'));
assert.ok(!article.includes('sudo rm -rf'));
const scope = page.getByRole('heading', { name: 'What an update snapshot contains', exact: true });
@@ -91,10 +94,50 @@ const { chromium, webkit } = require('playwright');
await page.getByRole('link', { name: 'rollback and backup scope', exact: true }).click();
await page.getByRole('heading', { name: 'Rollback', exact: true }).waitFor();
assert.ok(page.url().endsWith('/docs/AUTO_UPDATE#rollback'));
await page.getByRole('link', { name: '← All documentation' }).click();
const installLink = page.getByRole('link', { name: 'Install Pulse', exact: true });
await installLink.focus();
await page.keyboard.press('Enter');
const installRollback = page.getByRole('heading', { name: 'Rollback', exact: true });
await installRollback.waitFor();
const installText = await page.locator('article').innerText();
for (const text of ['Neither is guaranteed to include all active data',
'Update History is not a full-state recovery tool', 'matching data and keys together',
'preserve reversible copies', 'Do not replace live runtime data'])
assert.ok(installText.includes(text), text);
assert.ok(!installText.includes('auto-restores on failure'));
await installRollback.evaluate((heading) => heading.scrollIntoView({ block: 'start' }));
await page.waitForFunction(() => {
const heading = document.getElementById('rollback');
return heading && heading.getBoundingClientRect().top >= 0 && heading.getBoundingClientRect().top < 200;
});
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
await page.screenshot({ path: path.join(artifacts, `${engine}-install-rollback.png`) });
const snapshotLink = page.getByRole('link', { name: 'version-specific snapshot scope', exact: true });
await snapshotLink.focus();
await page.keyboard.press('Enter');
await page.waitForFunction(() => {
const heading = document.getElementById('what-an-update-snapshot-contains');
return heading && heading.getBoundingClientRect().top >= 0 && heading.getBoundingClientRect().top < innerHeight;
});
assert.ok(page.url().endsWith('/docs/AUTO_UPDATE#what-an-update-snapshot-contains'));
await page.goBack();
await installRollback.waitFor();
const stoppedLink = page.getByRole('link', { name: 'stopped-service recovery procedure', exact: true });
await stoppedLink.focus();
await page.keyboard.press('Enter');
await page.waitForFunction(() => {
const heading = document.getElementById('manual-rollback');
return heading && heading.getBoundingClientRect().top >= 0 && heading.getBoundingClientRect().top < innerHeight;
});
assert.ok(page.url().endsWith('/docs/AUTO_UPDATE#manual-rollback'));
assert.deepEqual(errors, []);
results.push({ engine, version: browser.version(), width, tone,
snapshotScope: true, noDestructiveRecipe: true, scopedCompose: true,
auditLink: true, dockerFragment: true, rollbackLink: true, errors });
auditLink: true, dockerFragment: true, rollbackLink: true,
versionQualifiedScope: true, noFullStatePromise: true,
installRollback: true, keyboardSnapshotLink: true, keyboardStoppedRecoveryLink: true, errors });
await browser.close();
browser = undefined;
}
+24 -5
View File
@@ -128,8 +128,9 @@ another update just to reproduce a failure.
Before any rollback, take a consistent private backup of the current state and
check the proposed backup's scope. Keep the failed installation and update
snapshots until recovery is verified. A rollback can revert settings, alert
changes and history written since the selected backup.
snapshots until recovery is verified. Returning to an older binary is not a
full-state recovery. Restoring a matching data snapshot can discard settings,
alert changes and history written since that snapshot.
### Automatic Rollback
@@ -144,8 +145,16 @@ health afterwards; a progress message alone is not recovery evidence.
If your installed version offers it, open **Settings → System → Updates →
Update History**. **Roll back** is offered for a successful in-app update with
a retained backup. The confirmation names the version before that update and
warns that later settings and alert changes will be reverted. Check that version
and backup before confirming; Pulse restarts to use the restored binary.
Pulse restarts to use the restored binary. Check that version and the actual
backup contents before confirming.
**Update History is not a full-state recovery tool.** Its restore scope depends
on the installed updater. Older updaters may replace `data/`, `config/` and
`.env` under the installation directory, but omit active data stored elsewhere.
Do not assume later settings and alert changes will be reverted, even if the
confirmation says so. If active data shares those installation paths, do not
restore it while Pulse is running; use the stopped-service procedure in
[Manual Rollback](#manual-rollback).
Older versions may not have this control. An absent button or backup does not
justify deleting data or creating an empty configuration. Docker server installs
@@ -159,13 +168,23 @@ low-space fallback. Use the `backup_path` recorded for the actual update in
`update-history.jsonl`, rather than guessing a timestamp. The updater retains
the most recent three snapshots; a `/tmp` snapshot may also be lost on reboot.
The in-app snapshot copies the running server binary, available `VERSION` and
Snapshot scope is version-specific. The published **v6.4.5** and
**v6.4.6-rc.1** in-app updaters use the limited installation snapshot described
below. Do not assume a backup made by another updater version has the same
contents or restore behaviour.
The limited snapshot copies the running server binary, available `VERSION` and
`.env` files, and available `data/` and `config/` directories under
`PULSE_INSTALL_DIR` (default `/opt/pulse`). It is not necessarily a complete
backup of the active `PULSE_DATA_DIR`, external metrics/audit stores, service
units, bundled agents or update helpers. Copy errors can leave a partial
snapshot: a recorded path is not proof that every file was saved.
A backup's existence, or the presence of a database file, does not establish
completeness, consistency or that Update History will restore it. A snapshot
that includes active data still needs a verified, stopped-service recovery
procedure; its presence is not permission to rewind live runtime stores.
Verify the backup against your effective service paths and update logs. Preserve
the active data directory, matching `.encryption.key`, audit history and its
signing key together, including custom paths and organisation directories. Use
+14 -2
View File
@@ -302,9 +302,21 @@ update. Use **Settings → Infrastructure → Install on a host** for a first in
or a v5-to-v6 in-place upgrade.
### Rollback
If an update causes issues on systemd installations, backups are created automatically during the update process.
An update error does not establish which version is running. Check the running
version and service health before retrying or rolling back, and preserve the
failed installation and update logs.
**Manual rollback**: In-app updates store backups under `/etc/pulse/backup-<timestamp>/`. The systemd auto-update timer uses a temporary `/tmp/pulse-backup-<timestamp>` during the update and auto-restores on failure.
In-app update snapshots and updater-script backups have different contents and
lifetimes. Neither is guaranteed to include all active data; an older binary
also may not understand data migrated by a newer version. Update History is
not a full-state recovery tool, and a recorded backup path is not proof of a
complete backup.
Check the [version-specific snapshot scope](AUTO_UPDATE.md#what-an-update-snapshot-contains)
and use the [stopped-service recovery procedure](AUTO_UPDATE.md#manual-rollback)
when data needs restoring. Keep matching data and keys together, verify the
backup privately and preserve reversible copies of the failed state. Do not
replace live runtime data or delete it to make a rollback fit.
---
+30 -12
View File
@@ -2,7 +2,8 @@
"""Exercise update help without touching a service, volume or Docker socket.
The synthetic commands prove scope and failure handling, not installed recovery.
The remaining checks bind the guide to the existing UI and snapshot writer.
The remaining checks bind the shipped help to the existing UI labels and
protect version-qualified recovery advice, not a particular snapshot writer.
"""
import json
@@ -76,7 +77,7 @@ class UpdateRecoveryDocsTest(unittest.TestCase):
return next(recipe for recipe in recipes if "compose" in recipe)
def test_mirrors_match_the_exercised_guides(self):
for name in ("AUTO_UPDATE", "DOCKER"):
for name in ("AUTO_UPDATE", "DOCKER", "INSTALL"):
with self.subTest(guide=name):
self.assertEqual(guide(name),
(ROOT / "frontend-modern/public/docs" / f"{name}.md").read_text())
@@ -89,25 +90,42 @@ class UpdateRecoveryDocsTest(unittest.TestCase):
self.assertIn(label, help_text)
self.assertIn("entry.action === 'update' && entry.status === 'success' && Boolean(entry.backup_path)", ui)
for phrase in ("successful in-app update", "retained backup", "version before that update",
"Older versions may not have this control", "later settings and alert changes"):
"Older versions may not have this control", "actual backup contents"):
self.assertIn(phrase, help_text)
self.assertNotIn("There is no rollback UI", help_text)
def test_snapshot_scope_is_not_claimed_to_be_a_full_data_backup(self):
help_text = " ".join(section("AUTO_UPDATE", "What an update snapshot contains", "###").split())
manager = (ROOT / "internal/updates/manager.go").read_text()
writer = manager.split("func (m *Manager) createBackup(", 1)[1].split("// ensureApplyTargetIsNewer", 1)[0]
self.assertIn('dirsToBackup := []string{"data", "config"}', writer)
self.assertIn('os.Getenv("PULSE_INSTALL_DIR")', writer)
self.assertIn('os.Executable()', writer)
for phrase in ("running server binary", "`VERSION`", "`.env`", "`data/`", "`config/`",
"`PULSE_INSTALL_DIR`", "not necessarily a complete", "active `PULSE_DATA_DIR`",
"Copy errors can leave a partial", "`backup_path`", "lost on reboot"):
self.assertIn(phrase, help_text)
# The current writer logs some errors and continues; existence alone
# must not be promoted to completeness in operator guidance.
self.assertIn('Msg("Failed to backup directory")', writer)
self.assertIn('return backupDir, nil', writer)
def test_online_rollback_never_promises_full_state_or_live_data_rewind(self):
help_text = " ".join(section("AUTO_UPDATE", "Rollback").split())
for phrase in ("Returning to an older binary is not a full-state recovery",
"Update History is not a full-state recovery tool",
"Do not assume later settings and alert changes will be reverted",
"omit active data stored elsewhere", "do not restore it while Pulse is running",
"[Manual Rollback](#manual-rollback)"):
self.assertIn(phrase, help_text)
self.assertNotIn("warns that later settings and alert changes will be reverted", help_text)
def test_snapshot_and_install_help_qualify_scope_without_claiming_future_support(self):
scope = " ".join(section("AUTO_UPDATE", "What an update snapshot contains", "###").split())
for phrase in ("published **v6.4.5** and **v6.4.6-rc.1**", "limited installation snapshot",
"backup made by another updater version", "completeness, consistency",
"not permission to rewind live runtime stores"):
self.assertIn(phrase, scope)
install = " ".join(section("INSTALL", "Rollback", "###").split())
for phrase in ("Neither is guaranteed to include all active data",
"Update History is not a full-state recovery tool",
"data migrated by a newer version", "version-specific snapshot scope",
"AUTO_UPDATE.md#manual-rollback", "matching data and keys together",
"preserve reversible copies", "Do not replace live runtime data"):
self.assertIn(phrase, install)
self.assertNotIn("backups are created automatically", install)
self.assertNotIn("auto-restores on failure", install)
def test_recovery_preserves_failed_state_keys_and_sidecars(self):
help_text = " ".join(section("AUTO_UPDATE", "Rollback").split())