Keep stderr and trailing blank records in service-state and running-PID admission. Incomplete reads must not authorise installation, restart, rollback or update success. Require production caller and real-process controls for both read boundaries; preserve existing recovery, trust and deadlines.
Change-source: pulse-maintainer
Keep the original failed transaction command distinct from any later cleanup command. Retain fixed operation labels and require portable controls for command, timeout, assertion, cleanup-only and successful receipts, without changing update transactions or disclosing command output.
Change-source: pulse-maintainer
Current exact-head CI exposes outdated PBS proof inventory, omitted bounded-download helper extraction, closed transport rejecting the production HTTPS flags, and Darwin's truncated comm column in Linux NAS fixtures. Preserve exact transport and process ownership, restoration and migration safety assertions while binding the controls to the actual producer.
Contract-Neutral: Test and offline CI-fixture alignment only; no installed runtime, trust, credentials, deadlines, source safety or public contract changes.
Change-source: pulse-maintainer
Give each installer and signature transfer a five-second connection and sixty-second total budget, ignore ambient curl settings and preserve HTTPS through redirects. Failed or partial downloads leave the prior install, service and identity unchanged; pinned signature admission and recovery deadlines are unchanged.
Change-source: pulse-maintainer
Compose upstream bed5052eb5 without replacing reviewed history. Keep the accepted one complete stdout/stderr observation and blank-line refusal instead of upstream second-sample replacement. Retain upstream real-systemctl fresh-host assertions and its platform-availability decorator alongside every reviewed failure-retention control. No release, installed recovery or LXC stop-cause claim.
Change-source: pulse-maintainer
The 2026-10-10 server service discovery change lists the two historical
unit names with one pattern read and refuses to continue when that read
exits non-zero. systemctl list-unit-files exits 1, printing nothing on
stdout or stderr, when none of the named unit files exist. That is every
fresh host, so install.sh refused with "Cannot identify the Pulse systemd
service: unit inventory failed" before it did any work. The first beta's
install.sh smoke test caught it on a clean Debian 12 container; no
release carries it, and v6.5.0 listed the whole inventory and grepped it.
The tests could not see it. Their systemctl double reports "no units" as
exit 0, and test_partial_failed_inventory_is_not_absence asserted that an
empty inventory with exit 1 must be refused, which is the defect itself.
Add one inventory read shared by the installer and the unattended
updater, and use it for the auto-update mask check, which had the same
hazard. It accepts exactly the silent exit 1 as an empty inventory, and
only after a second read confirms there is still no output and no
message. A timeout, a signal, any other status, or an exit 1 that carries
output or a message stays a failed inventory, so a real failure still
cannot pass as absence.
Make the doubles model systemd: a failure now prints its message, silent
exit 1 is absence, and a new test runs real systemctl --root over an
empty unit root for both scripts. Run against the unfixed scripts, the
four new fresh-host tests fail; the rest of the suite is unchanged.
The registered runtime proofs for install.sh and the unattended updater,
which drive this suite, now require the fresh-host controls to have run
and passed, so they cannot be skipped or deleted unnoticed.
Refs: #2785
Adapt rcourtman’s Pulse PR2855 (aef53d0d3e), which diagnosed systemd’s silent exit-one absence. The original contributor commit is unchanged and explicitly credited, not imported, amended or re-parented in this maintainer candidate.
One bounded stdout/stderr observation accepts genuine absence but retains failed first reads, other statuses and blank-line output. Include the existing registered installer/updater verification hooks, real-empty-root controls and missing-unit recovery double in this same source commit. This replaces the rejected foreign-history composition with a normal maintainer-reviewed implementation; no provenance policy, credential or host installation changes.
Source-attribution: rcourtman, https://github.com/rcourtman/Pulse/pull/2855, aef53d0d3e
Change-source: pulse-maintainer
Use a counted-NUL shell logger double instead of starting Python for every installer line. Keep production log(), complete pipe draining, all eight first/later/final-error recovery cases and the existing 45-second bound. Require exact argument-frame and failure-sequence controls in the Go acceptance wrapper.
Change-source: pulse-maintainer
Contract-Neutral: Confined installer test fixture optimisation only; production updater, signing, native oracle, recovery matrix and safety deadlines are unchanged.
The current native acceptance oracle can return success without a manager observation after comparing running executable bytes. Bracket the comparison with stable active process identity and compare both executables independently to the authenticated fixture, so a changing PID or equally wrong destinations cannot pass. Add required portable drift/refusal controls without changing installer authority or hosted admission.
Change-source: pulse-maintainer
Supply the connected manager double's fixed comparator and keep its simulated process readback explicit. Separate mandatory real-sink controls from service-state deadlines without removing cases or extending either 45-second bound.
Change-source: pulse-maintainer
Contract-Neutral: Test fixture correction only; the running-executable acceptance contract added by the parent is unchanged.
An installed target version and active service can conceal an old managed executable. Compare the actual process bytes between complete bounded MainPID observations before accepting an update or restored active service. Retain the backup and current service on uncertainty without an automatic restart or rollback grant. Register real owned-process and conditional transaction controls; keep synthetic/native and customer acceptance distinct.
Change-source: pulse-maintainer
Conditional Bash callers hid a failed logger write behind successful stdout. Attempt both sinks, return the first failed sink, and exercise the unchanged draining collector and recovery path with the real log primitive and confined external logger. Preserve the distinction between observed pipeline completion and native or customer recovery.
Change-source: pulse-maintainer
The exact-source installer suite exposed a result-reader defect: successful unittest methods emit phase markers before their final ok verdict. Retain complete unique method results across that output, reject missing/duplicate/mismatched identities, and keep the required Linux and portable-control verdicts unchanged.
Contract-Neutral: Test-result parsing only; no native fixture, platform admission, signing, installer, updater, recovery or customer contract delta.
Change-source: pulse-maintainer
The source-bound hosted receipt stops before any native phase because its new ephemeral signer is mode 0644. Establish owned regular-file and verified 0600 mode before signing, without adopting existing keys or relaxing OpenSSH. Keep Linux ELF execution in both native Linux jobs; macOS still runs the portable signer/admission/recovery controls, and the bridge requires the exact platform verdict instead of staging Mach-O as Linux.
Change-source: pulse-maintainer
Exercise real systemd and signed local current-source replacement/recovery in the existing real-embed job for #2785. Preserve service masks, persistent bytes, backups and owned cleanup; source controls do not claim published or LXC recovery.
Change-source: pulse-maintainer
Drain the installer pipe after a failed log write, retain its first observed failure across later successes and collect an unterminated final line. Reuse the guarded restoration path without changing signed-input, service-state, mask or prior-activity safeguards; bind the regression controls to package acceptance.
Change-source: pulse-maintainer
Capture both pipeline statuses before logging overwrites them. Keep recovery, signature and prior-service safeguards unchanged; cover installer, collector, simultaneous and successful observations without attributing a past host interruption.
Change-source: pulse-maintainer
Read bounded, complete unit state before mutation and confirm inactive state before rollback replacement. Keep the verified backup and stop automatic recovery when service state cannot be reconciled; preserve prior-active restoration, masks, signatures and configuration.
Exercise failed/partial reads, deadlines, no-op stops and restoration through confined production-shell fixtures and the existing signed transaction controls. Fix fixture path and manager observations so registered Go controls execute the actual source.
Refs: #2785
Change-source: pulse-maintainer
The helper selects and passes an explicit service name, bypassing installer discovery. Apply the same bounded exact-unit admission there and preserve its error before any prior-active query, backup, installer call or restart trap. Execute both producers and the real helper handoff in the registered unattended updater proof artifact.
Refs: #2785
Change-source: pulse-maintainer
Reject successful installers whose replacement cannot identify itself as the target community runtime. VERSION metadata and failed or unrecognised probes cannot establish acceptance; retain the existing verified rollback and prior-active restoration. Exercise the complete locally signed installer transaction and bounded executable read.
Change-source: pulse-maintainer
Require a successful bounded executable identity probe before release discovery. Keep Pro and unknown runtime editions untouched even when VERSION offers a usable community version, and do not defer a broken primary binary to a legacy one. Exercise the real shell classifier and main boundary with adverse controls and a TERM-resistant deadline.
Change-source: pulse-maintainer
Keep the original archive-copy metadata semantics while checking every snapshot and restore. A content-only copy would lose extended attributes. Exercise that recovery and reject a non-regular restored destination before byte comparison or activation; qualify the restart backstop comments for incomplete recovery.
Change-source: pulse-maintainer
The first connected attempt failed before installer execution because the fixture expected .sshsig while ssh-keygen creates .sig. Require that real signed output and serve it under the simulated release URL. Production signature verification is unchanged; retain the original failed proof.
Contract-Neutral: Correct the test fixture signature filename only; production updater and trust contracts are unchanged.
Change-source: pulse-maintainer
A successful rename is not by itself the recovered-byte postcondition. Keep the snapshot and suppress activation when the recorded destination is still a link or no longer matches the saved bytes; connected no-op and changed-destination controls exercise that boundary.
Change-source: pulse-maintainer
Reject directory destinations before the same-directory file rename, use portable mode inspection in the connected transaction tests, and teach the existing service fakes that stopping and failed activation are separate operations. The recorded first candidate proof remains bound to its own source.
Contract-Neutral: Preserve the reviewed backup and rollback semantics; portable utility invocation and faithful test boundary only.
Change-source: pulse-maintainer
The conditional updater caller disables errexit, but binary and VERSION backups and restore copies were best-effort and their only backup was deleted even when recovery failed. Require a private verified snapshot before installer execution, consolidate the three rollback branches, stage exact original paths and absence, and retain the backup without automatic activation when recovery is incomplete. Exercise the complete signed offline path and prior-active-only service recovery.
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
Two bugs from #1630 that combined to take installs down silently:
1. perform_update()'s install-failed rollback branch restored the backup
but never restarted pulse.service. Since the generated
pulse-update.service gates on ExecCondition=systemctl is-active,
every later timer run was then skipped and the install stayed down
until manual intervention. Restart is now guaranteed by a
service_was_active-guarded restart in that branch plus an
ensure_service_restarted RETURN trap so no exit path can miss it
(re-fix of #1323, originally c0b3a0e66, lost in 778a2577b and only
partially restored in 672e81985).
2. install.sh aborted under errexit when writing the /bin/update helper
on a read-only filesystem - after the new binary was installed and
the service stopped, landing in bug 1's no-restart branch. The stock
pulse-update.service uses ProtectSystem=strict, so /bin and
/usr/local/bin are read-only on stock unattended updates; transient
read-only remounts hit the same path. The helper write, PATH
appends, and the /usr/local/bin/pulse symlink are now idempotent and
non-fatal with a warning (install_binary_symlink).
Contract: deployment-installability now pins fail-closed service
availability for unattended updates and non-fatal writes outside the
hardened unit's writable set, with proofs in pulse_auto_update_test.go
and root_install_sh_test.go plus shell regression coverage in
scripts/tests/test-pulse-auto-update.sh (installer-exits-nonzero path)
and scripts/tests/test-install-update-resilience.sh (read-only helper
and symlink paths, verified under set -e, root-safe via ENOTDIR).
Fixes#1630
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Production telemetry (7d, 2026-07-17) shows a persistent tail of live
installs on 6.0.0-rc.x and 6.0.3/6.0.4 while 6.0.5 has been stable since
2026-07-09. Root cause for the reachable stranding class: release
selection trusted GitHub's created_at ordering, and this repo interleaves
v5-line maintenance releases with v6 releases (v5.1.36 was created the
day before v6.0.5). Whenever a v5.1.x release is the most recently
created stable:
- stable channel (getLatestReleaseForChannel) returned the first
non-prerelease in list order -> "no update" for every v6 install
until the next v6 release ships
- the RSS rate-limit fallback returned the first feed entry matching the
channel, same failure, both channels
- the unattended updater trusted /releases/latest, which GitHub defines
as most-recently-created stable -> timer no-ops in the same window
All three now select the highest eligible version: the API path tracks
newest stable + newest prerelease in one pass (prerelease = GitHub flag
OR tag shape), the feed fallback picks the max matching the channel, and
pulse-auto-update.sh scans /releases pairing each tag with its own
draft/prerelease flags before the fail-closed shape filter, keeping
/releases/latest as fallback. helm-chart-* tags fail semver parsing and
are dropped everywhere.
Verified not broken (pinned by new tests): rc-channel installs are
offered the newer stable (6.0.0-rc.6 -> 6.0.5) and move onto the next rc
line when one opens (-> 6.1.0-rc.2); the auto-update prerelease filter
(fail-closed is_prerelease_tag, metadata-flag refusal) still refuses
prerelease targets on stable. Go tests fail on the pre-fix code
(returned 5.1.37 / "no update").
Contract: deployment-installability now pins version-max release
selection with proofs in manager_stranded_upgrade_test.go and
pulse_auto_update_test.go.
The in-app updater and the unattended timer both target the public
rcourtman/Pulse community assets, so a Pro install that used them was
silently downgraded to community. Guard 2 (983a89326) blocked in-app
apply on the Pro edition, which stopped the downgrade but left Pro
installs with no update path except manual portal downloads. That
friction is a plausible driver of the runtime split: as of 2026-07-08
only 10 of 66 active paid licenses have any Pro-runtime install.
Root fix: the compiled Pro binary now checks and applies updates
through the license server download broker (GET /v1/downloads/pulse-pro
with the installation token and instance fingerprint). The check
compares against the broker's pinned private release instead of GitHub,
respecting the stable/rc channel guard. Apply re-resolves fresh signed
R2 URLs at apply time, verifies the archive against the same pinned
pulse-installer SSHSIG key plus the broker manifest sha256, and refuses
GitHub-shaped download URLs outright. An unactivated Pro binary still
refuses with the portal fallback. The community edition path is
unchanged.
The update banner restores in-app apply for auto-updatable Pro
deployments and keeps the portal instructions for deployments the
updater cannot drive (Docker). scripts/pulse-auto-update.sh now skips
when the installed binary reports Pulse Pro so the unattended timer can
never reinstall community over Pro.
Note: internal/updates/pro_update.go and manager_pro_update_test.go for
this change landed one commit early inside 313552deb via a parallel
session committing a shared staged index; this commit completes the
wiring they belong to.
Refuse prerelease-shaped tags and explicit GitHub prerelease responses in the unattended stable updater before installer invocation.
Add installability tests and proof routing for the auto-update prerelease refusal guard.