With the host-interface canary in place, v6.4.5-rc.4 passed the
schema-v7 systemd and rootful Docker lab (run 36594651903), but the
attestation step then failed with Permission denied on receipt.json,
which the privileged lab writes as root into the bind-mounted evidence
directory. Hand that directory to the runner user after a passing lab
and before attestation, without touching its content.
The immutable RC systemd lab failed before installed acceptance because
its --network none container had no non-loopback IPv4 address to prove
the helper's private network cannot reach. Add a veth canary wholly
inside that namespace and assert no default route before the lab runs.
Also check out the repository in the demo resolver before it calls
scripts/write_github_output.py, so verification-only dry runs reach
their demo checks.
Split out of #2323 so the v6.4.5 promotion is not held behind the
unrelated action-review UI work in that PR.
The weights from #2324 came from one local run and left the api shards at
4.5, 8.8, 17.1 and 12.5 minutes on CI. Most of the 17.1 was not test time.
That shard ran `-run . -skip <1231 names>`, and the test binary caches only
its last compiled pattern, so alternating the two recompiled the 62 KB skip
regex for every test and subtest (about 50 ms each under -race locally, 30s
against 1.3s for 300 trivial tests). Two runs put that shard at 887s and
875s against roughly 450s of tests. The skip path now passes -skip alone.
Every api shard now runs go test -json through
.github/scripts/record-internal-api-test-seconds.py. It prints what plain go
test would (package lines and the output of failing or unfinished tests),
exits non-zero on any failure behind pipefail, and writes each top-level
test's seconds to an internal-api-test-seconds-N artifact that is uploaded
even when the shard fails. .github/scripts/refresh-internal-api-test-seconds.py
rebuilds the weights file from those artifacts (gh run download, median
across runs, DEFAULT_WEIGHT set to the mean of the unlisted tests, which the
selector now reads from the file).
Until those artifacts exist, the weights are a local -race -json run with
each region of go test's order scaled to what CI reported for it.
Equal-count quarters averaged 107, 437, 100 and 787s over four runs, and the
weighted cut gave 131s for the first 1187 tests, 350s for the next 23 and
608s for the last 21 over two. The last quarter and the last 21 tests
disagree on how the tail splits, so the tail takes the midpoint of an exact
fit and a pooled factor.
With these weights four shards still predict a slowest shard near 10
minutes, and a 20% slower runner on the tail would take it past the 11
minutes of Frontend and rest-1. Five shards predict about 131, 325, 280-340,
330-355 and 330-365s of tests, or 4.5 to 8.5 minutes a job with about 2.3
minutes of setup, so API_SHARD_COUNT goes to 5. Every test still runs
exactly once in contiguous go test order, and all five slices passed locally
from their new starting points. The required Backend tests (api) verdict
keeps its name.
Relay left public checkout on 2026-09-29: it only ever connected the
Pulse Mobile app, which retires on 31 March 2027, and existing Relay
subscribers now carry Pro entitlements. The app still sold it. Every
Community install saw a "Get alerts on your phone ... Available with
Relay and Pro plans" upgrade panel on Alerts destinations, the plan
screen offered a Relay card with "Remote web access via Relay", and the
settings section was called Remote Access although Relay never reached
the web UI.
The Alerts push panel now renders only on instances that have the relay
feature, with no upsell. The settings section, nav, header and locale
catalogs say Pulse Mobile and carry the retirement date. Community sees
only the Pro comparison card and Relay-tier licenses see none; gated
mobile features name Pro as their minimum plan. The relay feature is
labelled "Pulse Relay (Mobile Connection)" in the catalog, plan copy no
longer claims remote web access, the backend pairing diagnostics point
at Settings > Pulse Mobile, and the user docs, including PRIVACY.md,
state that Relay does not provide remote web UI access.
The script smoke test still required "Pulse Mobile pairing for handoff"
in PRIVACY.md, the wording the previous commit replaced when it removed
the unbacked "secure remote web access" claim. Pin the new description
and forbid the remote web access wording in both copies.
The four Backend tests (api-N) shards took equal-count quarters of go
test's list, which left them at 4.0, 10.6, 4.0 and 16.8 minutes on the
first PR run, and at 1.9, 8.0, 1.5 and 10.3 minutes of go test time on
the following main push (run 36560070496). Almost all of it sits in a
few tests. One contract mock chart test runs about six minutes on the
runner, and the 28 integration server tests at the end of the list take
about twenty seconds each, so the fourth quarter held nearly half the
package.
Shards are still contiguous runs of go test's own order, since some
tests depend on package state from their neighbours, but the cut points
now come from estimated seconds.
.github/scripts/select-internal-api-shard.sh weighs each test from
.github/scripts/internal-api-test-seconds.txt (the 199 tests at 0.5s or
more, seeded from a local -race -json run scaled to that main run's
shard totals, with a 0.05s default for anything unlisted), binary
searches the smallest per-shard budget that fits the list into four
slices, and fills up to it. The weights only move cut points. Every test
is assigned exactly one shard in a single pass, so a stale file can
unbalance shards but never drop or repeat a test, and no shard is ever
empty.
With today's list the slices hold 1187, 23, 2556 and 21 tests, estimated
at 1.9, 6.9, 6.5 and 6.7 minutes of tests, or about 9.5 minutes per job
with the roughly 2.6 minutes of setup and race compile. The 2556-test
slice would need a 124 KB -run pattern, so a shard passes -skip of the
other shards' tests whenever that is shorter, which runs the same tests
in the same order.
Across the last 25 bot PRs the median open to merge time was about 33
minutes for one PR and about 45 when three opened together. The critical
path was the required Backend tests (api) check, where one go test -race
./internal/api step took about 27 of its 31 minutes. Every other required
check finishes within about 12 minutes.
internal/api now runs as four Backend tests (api-N) shards. Each shard
lists the package's tests with go test -race -list from the commit under
test and runs one contiguous quarter of that list in go test's own order,
so every test, including ones added later, runs in exactly one shard, and
a shard that resolves no tests fails. The split is contiguous rather than
interleaved by name because some internal/api tests depend on package
state left by the tests just before them. A round-robin split by sorted
name failed dozens of tests locally (admin bypass and session store
globals), while the four contiguous slices and the full run all pass. A
Backend tests (api) verdict job keeps the required check name and fails
unless every shard succeeded. Backend tests (rest-0) and (rest-1) keep
their names and package split. Local non-race timings put the heaviest
slice at about 42 percent of the package, so the api check should drop
from about 31 minutes to roughly 14.
Core E2E no longer runs the non-gating probation tier on pull requests.
It could not gate a PR and the promotion rule counts only main runs, so
on a PR it only held each of the eight shard runners about eight minutes
longer, which fed the runner queueing seen with concurrent PRs. Push and
manual runs still execute it.
Go test steps still run for frontend-only changes. Go tests read
frontend sources directly (internal/api contract tests,
internal/unifiedresources walking frontend-modern/src, and the
internal/telemetry repository-wide wording scan), so skipping them by
path would drop real coverage.
Dispatch the post-publication check from protected main while binding its packet to the immutable RC tag. Accept only canonical Pulse checkout origins before normalising the spelling required by the attester, and document the deployment contract and source guard together.
Change-source: pulse-maintainer
The resolver refused a stable promotion until its candidate had soaked for
72 hours, or 168 for a minor. Few installs run previews: 35 of about 7,900
active installs ran v6.4.5-rc.3 in the week to 2026-09-28, so a longer
soak saw little the first day did not, while 75 percent of installs sat on
a stable build a month old. Richard set a 24 hour soak for every release
on 2026-09-28.
The minimums drop to 24 hours for minor and patch alike, the error text
reads the constants instead of repeating numbers, and the workflow and
trigger prompts, the promotion policy and the deployment contract say the
same. The content-drift check and the hotfix exception are unchanged.
TestInstallSHRecoversV5ProcessArgsWithoutProcfs stubs the running agent
as pid 4242 and stubs ps, but running_agent_arg_stream reads
/proc/<pid>/cmdline whenever it exists and only falls back to ps when it
does not. On a Linux runner where pid 4242 happened to belong to another
process, recovery parsed that process's arguments and the test failed
with NOT_READY (seen in Backend tests rest-1). It never failed on macOS,
which has no /proc.
Wrap the real function so this test always resolves against an empty
proc root, which is the no-procfs host it describes. Also extract the
env-stream helper it was calling without, and fail on NOT_READY directly
since "READY" is a substring of it and never caught a failed recovery.
A failed update check never reaches the update history, because nothing
was applied, so telemetry could not tell an install whose check is broken
from one that was offered an update and ignored it. The 6.4.3-rc.1 installs
looked healthy while their check failed on every call (#2282).
The update manager now records a closed outcome for every check on the
install's effective channel (up_to_date, available, no_release,
rate_limited, network_error, metadata_error, skipped, error), tagging
lookup failures without changing their error text, and ignores previews of
another channel from Settings. Telemetry schema v18 exports update_channel,
update_check_outcome and update_available. No version, URL or error text
leaves the install. The Settings preview type, privacy disclosure, adoption
report and subsystem contracts are updated to match; the Pulse Pro receiver
change lands separately and the parity check passes against it.
The isolated coverage-v8 5 proposal cannot install beside Vitest 4 because its peer range requires Vitest 5. Ignore only version majors for both packages until a reviewed frontend migration can update and prove the pair together; retain grouped security updates and minor/patch updates.
Change-source: pulse-maintainer
Keep ESLint 10 and lucide-solid 1.x out of automatic version updates while their lockfile and icon-import contracts fail. Preserve minor and patch updates and guard the rule in Dependabot policy tests.
Change-source: pulse-maintainer
The release-promotion guard still expected the old installer-link wording,
so it rejected the corrected helper-ownership and version-pinning guidance.
Separate the installer safety assertions from support-pack coverage and
check the signed fallback, rollback applicability and version verification.
Rejected active-alert writes still reached history, firing events and
notification dispatch. Make admission explicit across detector and restore
paths so repeated suppressed observations cannot create those side effects.
Reconcile restored alerts when persisted resource policy attaches at startup,
reconcile again after native aliases become canonical, and refresh shared
state before those alerts reach clients.
Preserve occurrence identity when retained alerts re-fire, including durable
history reconstruction and delayed lifecycle replay.
Keep retained table and card components mounted when scrolling shifts the
visible window, so policy editors do not lose unsaved state. Show actual
future maintenance times and the paused-attention effect.
Keep synthetic host machine identities stable across restarts so saved
operator policy remains attached during verification.
Qualify server update and rollback instructions by helper ownership, and
stop signed installer recipes before execution if a download or signature
check fails.
Refs #2237
setup.sh resolves the image pins in .env to digests once, so upgrade.sh
only ever re-pulled the release a provider first installed. Moving to a
new release meant finding the new digests by hand, so none of a release's
fixes, and no new feature such as the portal Plan tab, reached an
existing install.
Run from a newly extracted release bundle, upgrade.sh now works on the
existing install: it resolves the bundle's control-plane and runtime tags
to digests, runs the status gate, preflight and verified backup with the
new release's control plane through a shell override while nothing on
disk changes, then keeps a copy of .env, installs the bundle files and
writes the new pins before starting the new images. --keep-image-pins
keeps hand-set pins.
It also pulls the tenant runtime image before the first status check,
which otherwise failed every upgrade to a new CP_PULSE_IMAGE with "not
present locally", including the documented pin-editing route.
Exercise two provider installations with the same tenant ID, unowned-network collision and cleanup isolation. Gate the release-line push before packet freeze and repeat against digest-verified candidate images during container qualification. Update the deployment-installability contract and pin both workflow gates in the release-asset verification suite.
Change-source: pulse-maintainer
Keep first-run evaluation and summary guidance conditional on actual Plan availability and confirmed active limits. Remove the ordinary manual-key purchase prompt while retaining the separate custom-license binding path, and update the installability contract and focused assertions in the same commit.
Change-source: pulse-maintainer
setup.sh still told an evaluating provider to "request an upgrade" at
/msp.html#request, a form that waited on a human reply with a checkout
link. The portal now sells plans itself (Plan tab, Stripe checkout,
applied in seconds), so both the evaluation log line and the closing
summary send the provider there. The summary also stops printing the
lease signing public key as something the licence "must bind": the
portal purchase binds it without a copy step, and the key is still
available through --print-lease-signing-public-key for a custom licence.
Start the provider services after licensed compose validation so the printed bootstrap and portal link have a live control plane. Correct the first-run prompt to name only operator-provided values and retain an install contract regression test.
Change-source: pulse-maintainer
Every stable cut re-pins CANONICAL_DEFAULT_PULSE_VERSION in
scripts/install-docker.sh from the RC to the stable version, and the
stable install-metadata test requires it. The drift allowlist omitted
that file, so no release candidate could be promoted without
hotfix_exception. The rule arrived on 2026-09-01, after v6.4.1, so no
stable promotion has run under it yet. v6.4.5 would be the first to fail.
The installer is shipped content, so the resolver accepts it only when
its diff against the candidate changes nothing but that pin line. Any
other installer change still counts as drift.
Go _test.go files are never compiled into a shipped artifact. A stable
cut can need test-only corrections, as v6.4.5 needs for the RC-derived
install-metadata contract, so they no longer count as unsoaked content.
Tests cover the allowed shapes, the refused near misses, and the pin
check against a real git diff.
Keep the canonical release and provider-MSP frontend builders on the proposed immutable amd64 Node 24 digest. Bind the same-commit change to the deployment contract, release-build metadata proof and provider-MSP rollout-control proof; hosted build and installed rollout remain separate evidence.
Change-source: pulse-maintainer
The Backups drawer's Identity and History target is the merged PBS host resource. Correlation was recomputed from each snapshot with no memory, so a live refresh that briefly omitted the correlated host row made the drawer fall back to the PBS service target (agent/<service key>), which has no host history. The target flicked between the host series and the service key until the host row returned.
Retain the last resolved correlation per PBS server and reuse it only across such an omission, and only while the remembered host is still fresh relative to the server, so a genuinely removed or replaced host is not advertised indefinitely. A host row that is present but ambiguous still declines, as before.
Adds unit coverage for retention, staleness and pruning, a drawer regression test, and extends the real-browser guard and fixture to omit the host row and assert the identity rows and History chart survive.
Change-source: pulse-maintainer
action_consumer_manifests.json pins each release-consumed action to an exact SHA plus the upstream action.yml sha256, and release_promotion_policy_test.py requires every workflow step to match. Dependabot's github-actions group bumps those SHAs, so every group proposal fails the manifest contract and cannot be repaired offline. Ignore the eight governed actions for version updates and guard the policy against the manifest so pin refreshes stay explicit reviewed work. This resolves the recurring failing actions-minor-patch proposal (#2139).
Change-source: pulse-maintainer
Advance the managed go-minor-patch group with its required transitives:
golang.org/x/crypto 0.56.0->0.57.0, golang.org/x/term 0.45.0->0.46.0 and
modernc.org/sqlite 1.53.0->1.59.0 (libc 1.75.7, memory 1.12.1, x/text 0.42.0).
All other direct versions are unchanged.
Raise the dev-runtime dependency-manifest floors to the qualified releases and
add the storage-driver integrity floor so a silent downgrade fails. Record the
change in the deployment-installability contract.
The batch is routine managed dependency maintenance, independent of the excluded
6 September SQLite SIGSEGV investigation. The NormalizeSegment benchmark delta is
a code-layout measurement artifact (pure function; caller benchmarks do not
corroborate) and the failed advisory result is preserved.
Change-source: pulse-maintainer
Dependabot #2100 and #2101 propose TypeScript 5.9.3 -> 7.0.2, but the checked-in @typescript-eslint/eslint-plugin@8.70.0, @typescript-eslint/parser@8.70.0 and typescript-eslint@8.70.0 each declare a peer dependency of "typescript": ">=4.8.4 <6.1.0". The proposals therefore fail ERESOLVE and all frontend E2E shards, with no security advisory behind them.
Add a semver-major ignore for typescript in the npm ecosystem so majors stay explicit work alongside a lint-toolchain upgrade, and update the config guard test to pin both the browser-runtime and TypeScript ignores.
Change-source: pulse-maintainer
The install-metadata contract encoded only the emergency no-RC stable-patch shape. A stable patch with a same-version RC promotes the exercised candidate (promotion_mode stable-rc-promotion), so the contract must assert that shape and derive the rollback target from the last published stable.
previousStablePatchVersion hardcoded only v6.4.2 as unpublished, so v6.4.5 rolled back to the never-published v6.4.4. Add v6.4.3 and v6.4.4, which only ever shipped as prereleases, so v6.4.5 resolves to the published v6.4.1 that the promotion resolver also derives.
Add stablePatchPromotedPrerelease to detect the same-version candidate from the retained release notes, and branch the stable-patch install-metadata tests between the emergency and RC-derived shapes. Unit tests pin the rollback derivation and the candidate detection.
Change-source: pulse-maintainer
The FreeBSD rc.d branch printed a log hint pointing at the system log,
but pfSense has no /var/log/messages (its system log is
/var/log/system.log). More importantly, daemon(8) does not capture the
child's stdout and rc.d has no journal, so the agent's zerolog output was
never written to any file: correcting the path alone would still point
users at a log with no agent entries.
Give the agent its own rotating log via --log-file on FreeBSD (both the
TrueNAS CORE and pfSense/OPNsense/vanilla FreeBSD branches) and point the
completion hint at it, matching the Unraid/QNAP/TrueNAS-Linux convention.
The shell installer is a canonical path shared by agent-lifecycle and
deployment-installability, so record the FreeBSD log-capture obligation in
both subsystem contracts.
Refs #2123.
Change-source: pulse-maintainer
A live resource snapshot can briefly omit a field that gates a drawer tab, for example a merged metrics target during a refresh. The tab guard reset the active tab to Overview whenever the selected tab was missing, so the reported Proxmox -> Backups History tab silently jumped back to Summary on data refresh. Reset only when the drawer starts showing a different resource; each tab body already renders an availability notice while its tab is transiently unavailable. Adds a regression test covering a snapshot that drops and restores the metrics target, a source guard in ResourceDetailDrawer.history.test.tsx, the unified-resources contract update, and a fresh frontend browser-verification receipt from scripts/check-drawer-tab-retention.cjs (the same check times out on the pre-fix source).
Change-source: pulse-maintainer
The Backups drawer renders Discovery and Metrics Target from the merged host resource, so a snapshot-driven correlation change would silently flip those labels. Add discoveryTarget to the retained real-browser fixture and assert both identity rows stay present and byte-identical across reordered, timer-driven snapshots and datastore switching. The guard passes on the current source; it does not reproduce the reporter's live target-spelling flicker.
Change-source: pulse-maintainer
Extend the PBS History regression to PBS-only and bare-metal PVE/PBS host shapes, stable and reordered snapshots, and desktop/mobile widths. Retain guest coverage and assert drawer identity, tab selection and history targets. This does not claim the separate reported tab reset is reproduced or resolved.
Change-source: pulse-maintainer
Detach each rendered datastore from the nested PBS snapshot so independent row reconciliation cannot overwrite another datastore's identity. Cover switching and refreshed History in the component regression and real-browser fixture for #1723.
Contract-Neutral: Restore existing PBS datastore row identity during snapshot reconciliation; no API or canonical ownership change.
Change-source: pulse-maintainer
A standalone source=pbs host row and the PVE guest carrying the same agent are one machine. Collapse correlation candidates that share an agent identity and prefer the guest metrics target, whose persisted host series the Backups drawer renders; the PBS service target names the service key and has no host history. Distinct agent identities, or rows without a provable identity, still decline to choose so the drawer cannot substitute an unrelated host. Re-compose the reviewed runtime change with its three subsystem contracts, offline browser fixture, check script and a content-addressed browser receipt in one governed commit.
Change-source: pulse-maintainer
Register 8b56fc72e4 with its contract-completion commit so canonical governance evaluates the reviewed Proxmox LXC linked-agent memory runtime change and its monitoring and unified-resources contract sections as one historical unit.
Change-source: pulse-maintainer
The secure-runtime RC qualification workflow (commit 79d87a4a2c) resolves the branch the control plane maps the version to and passes it as --main-ref, but secure_runtime_attestation_v6.verify_canonical_main_identity still required origin/main and required the candidate to be an ancestor of origin/main. Every release-train candidate therefore failed the pre-authentication step before any check ran; v6.4.5-rc.1 failed at 'Pre-authenticate exact qualification packet' with 'committed-main classification requires canonical origin/main' (run 35680955017), and all three historical runs failed.
Accept exactly origin/main or the release train's origin/release/vX.Y, verify the ref against the canonical origin's live branch tip, and check candidate ancestry against that same ref. The release-candidate classification is unchanged. Update the deployment-installability contract and the v6 unit tests, including a mapped-release-branch acceptance case.
Change-source: pulse-maintainer
The Alerts overview alert-card footer is an items-center flex row whose first child, the Started timestamp, carried its own mt-1. Under items-center that margin shifted the Started run 2px below the adjacent delivery-status run, which is the small text misalignment the reporter underlined in #2119. Remove the child margin; the footer row already carries the top margin, so the two runs share a baseline.
Add a presentation regression test and an offline browser proof that measures the text rects of both runs at desktop and narrow widths.
Contract-Neutral: Behavioral footer-alignment fix; no public-contract or subsystem-shape delta.
Change-source: pulse-maintainer
Register the two #2140 runtime commits with their contract and proof completion commit so the canonical completion guard evaluates each runtime change together with the follow-up that records its contracts and verification artifacts, as one reviewed unit. Registry-only change; no runtime behaviour.
Change-source: pulse-maintainer
Lands contributor PR #2067 (head 5eeea72a3a, author rcourtman) as a single maintainer commit on main. The PR documents the authenticated /api/resources/{id}/operator-state maintenance window contract and repairs GitHub-compatible documentation heading fragments.
Composed from the reviewed web-product candidate tree 68465bc48d (candidate 20260921T054028Z-web-product), which resolved the conflicts against main and replaced the unavailable github-slugger dependency with a dependency-free GitHub-compatible heading slugger. The tree is unchanged from that reviewed candidate; only the commit shape differs so the fdb2be15be..HEAD provenance range contains no external PR commit.
Validation retained from that candidate: vite build passed, check-bundle-size.mjs passed (vendor within budget), 61 focused vitest tests passed, and the offline Playwright docs-fragment-navigation run passed with a content-addressed browser-verification.json receipt based at fdb2be15be.
Change-source: pulse-maintainer
Pair the windowed item-height estimate repair (6f0dc177c8) with its contract completion commit (8738eb1bff) so the per-commit canonical guard evaluates them as one reviewed unit.
Change-source: pulse-maintainer
The virtual window measured only the top spacer's first sibling. On grouped
surfaces that sibling is a short group header, so the estimate collapsed to
the header height and the mounted window advanced roughly one row per header
height of scroll. The Alert Thresholds card list then dropped a host's rows
while scrolling and its Ctrl-F match count oscillated.
Measure the tallest of the first few rendered siblings so a leading header
cannot collapse the estimate, while uniform tables still measure their real
row height. Add a regression test and offline browser proof.
Change-source: pulse-maintainer
Register the server startup-stall watchdog runtime commit and its shared
follow-up contract/proof commit in the canonical completion history so the
per-commit guard evaluates the pair as one reviewed unit.
Change-source: pulse-maintainer
Integrate the reviewed failed-staging config-backup discard repair (07d54cd345)
with its deployment-installability contract, installtest proof and registered
completion pair (ccfd22da7f, b36e9609dd). The candidate auto-update RETURN-trap
repair (9b004cc787) duplicates the already-integrated 564c235e32, so the merge
keeps the canonical trap implementation and both regression suites and retains
both registry entries. Change-source: pulse-maintainer
Change-source: pulse-maintainer
Register the two update-path runtime commits and their shared follow-up
contract/proof commit in the canonical completion history so the per-commit
guard evaluates each pair as one reviewed unit.
Change-source: pulse-maintainer
Record the deployment-installability contract section for the auto-update
RETURN-trap and failed-staging config-backup repairs, and add the Go proof
that exercises the extracted perform_update and asserts the trap does not leak
into a later function return. This completes the two runtime commits for
canonical governance as one reviewed unit each.
Change-source: pulse-maintainer
Register 564c235e32 (runtime) with bb25e3ec03 (contract + Go proof) as a
reviewed completion unit so the per-commit canonical guard evaluates them
together rather than rejecting the runtime commit for a missing same-commit
contract and verification artifact.
Change-source: pulse-maintainer
Follow-up to 564c235e32 (fix(update): disarm perform_update RETURN trap after
it runs). That runtime change touches the deployment-installability subsystem,
so this commit adds the matching substantive contract section in
docs/release-control/v6/internal/subsystems/deployment-installability.md and
the registry-accepted deployment-script-runtime proof in
scripts/installtests/pulse_auto_update_test.go
(TestPerformUpdateDoesNotLeakReturnTrap). The pair is registered in
scripts/release_control/canonical_completion_history.json by the next commit.
Change-source: pulse-maintainer
backup_existing snapshots the configuration before the update stages, but a
staging disk-headroom failure then exited and left that snapshot behind. On a
small root filesystem each automatic retry added another full backup and made
the low-space condition progressively worse until updates could no longer run
(#2127). Record the snapshot and remove it when download_pulse aborts at the
headroom check, before anything was replaced. Add regression coverage.
Change-source: pulse-maintainer