The smoke gate workflow exists from commit 065ebdb27 but until it is
called from create-release.yml it does not actually protect any release.
That is exactly the regression class that let rc.1 → rc.5 ship with a
broken install.sh: nothing in the release pipeline exercised the
documented secure-install flow against the published GitHub Release URL.
Wire install-sh-smoke.yml as a downstream workflow_call after
validate_release_assets succeeds. Gated on
historical_asset_backfill_only != 'true' since asset-backfill flows
re-upload to an already-published release and the smoke would just
re-confirm what hasn't changed.
Pre-install structural checks were verified locally against rc.5 — the
gate correctly fires the banner / agent-banner / --version handler
assertions against the broken release. The end-to-end container portion
(privileged systemd boot, install.sh execution, /api/health, /api/version
match) will run for the first time on the next release that publishes
through this workflow; existing retry loops on systemd readiness,
service activation, and health endpoint absorb transient runner flakes.
Add install-sh-smoke.yml to the deployment-installability canonical files
and to the release-promotion proof policy's match_files, and add
scripts/installtests/build_release_assets_test.go to that policy's
exact_files (matching the existing pin set for related policies in the
deployment-installability subsystem). Update subsystem_lookup_test.py
fixtures that pinned the exact_files list literally.
Pinned the create-release.yml wiring in build_release_assets_test.go
alongside the validate-release-assets wiring so the smoke step cannot
silently be unwired.
Document the gate's contract responsibilities in
deployment-installability Extension Point 2.
Across v6 rc.1 → rc.5 the published install.sh asset was the agent
installer rather than the server installer, and the README's pinned
ed25519 key did not verify what the pipeline actually signed. The first
broke `bash install.sh --version` and the in-product Update button; the
second silently failed the README's secure-install ssh-keygen step.
Neither was caught by CI because every existing gate operated on the
local release/ build, the Docker image, or the helm chart — nothing
exercised the documented LXC/systemd install commands against the
published release URL.
scripts/validate-release.sh now catches asset-identity drift at build
time. This workflow catches the rest of the regression class — anything
that breaks the actual install at runtime — by running the documented
flow end-to-end against the published release.
What it does:
- Downloads install.sh, install.sh.sshsig, and the linux-amd64 tarball
from releases/download/<tag>/.
- Extracts the README's pinned pulse-installer ed25519 key and runs the
exact ssh-keygen -Y verify command from the README's secure-install
snippet against the downloaded asset.
- Re-checks the server-installer banner, the --version) arg handler, and
the absence of the agent banner — same pins as validate-release.sh, but
now against what GitHub is actually serving (not just what was built
locally).
- Boots jrei/systemd-debian:12 privileged, runs
`bash install.sh --archive <tarball> --disable-auto-updates` from
inside, waits for systemd pulse.service to become active, hits
/api/health, and asserts /api/version reports the expected version.
--archive mode is used rather than --version so the workflow doesn't
depend on install.sh's self-refetch loop (the re-fetched bytes are the
ones we already validated). Auto-updates are disabled to avoid the timer
unit doing anything during the smoke run.
Triggers are workflow_dispatch + workflow_call only. Wire it into
create-release.yml after the next RC validates it green.
Pinned in build_release_assets_test.go so silent deletion or weakening
of any critical assertion (signature verify, banner check, /api/health
hit, version match) trips the test.
The README's secure-install snippet has pinned the wrong ed25519 key
since commit a60fa03d7 (April 22, 2026), so v6 rc.2 through rc.5 all
shipped with a documented verification step that does not work.
I downloaded the published rc.5 install.sh + install.sh.sshsig and
ran ssh-keygen -Y verify with both candidate keys:
Ds21c5... (README's pinned key) -> Could not verify signature
MZd/DaH... (key embedded in install.sh and pulse-auto-update.sh) -> OK
Customers who actually followed the README's secure-install path saw
"Could not verify signature" and aborted. Most users curl-pipe the
script unverified so the drift went unreported.
Replace the stale key in README.md and docs/INSTALL.md with the actual
pipeline signing key (MZd/...).
Add a validate-release.sh smoke that extracts the README's pinned key
and runs the exact ssh-keygen -Y verify command against the signed
install.sh.sshsig. Any future drift between documented key and actual
signing key fails the release before publish.
Lock both the correct-key presence and the stale-key absence in
build_release_assets_test.go for README and docs/INSTALL.md so a manual
edit cannot regress the docs back to the broken state.
Every v6 RC (rc.1 through rc.5, ~30 days) shipped the wrong install.sh.
build-release.sh was copying the rendered AGENT installer into
release/install.sh, but adapter_installsh, scripts/pulse-auto-update.sh,
the root install.sh's own --rc/--stable/--version flows, and the README
quickstart all fetch that asset and run `bash install.sh --version vX.Y.Z`.
Since the agent installer rejects --version with "Unknown argument", the
LXC quickstart, the in-product "Update Pulse" button on systemd/proxmoxve
deployments, and the pulse-auto-update.sh systemd timer were all broken
for every RC.
Fix the build-release.sh copy to publish the root server installer.
The agent installer continues to ship inside tarballs at ./scripts/install.sh
and inside Docker images at /opt/pulse/scripts/install.sh, and is served
at the running Pulse server's /install.sh endpoint — none of those paths
change. Only the top-level GitHub Releases asset moves from agent to server.
Update build_release_assets_test.go to lock in the new publishing rule
and ban the reverse drift, replacing the March 18 "legacy root install.sh"
guard that was the original mistake.
Add a validate-release.sh smoke that catches the regression mode this hid:
the published install.sh must have the Pulse server banner, the --version)
arg handler, must not contain the agent banner, and `bash install.sh --help`
must print the server installer's version-pinning help line. These checks
run as part of validate-release-assets.yml against the post-publish asset
bundle so a future swap back cannot slip through.
Document the asset identity rule and the validate-release.sh guard in the
deployment-installability contract so any future change to the publishing
pipeline has to update the contract or trip the shape guard.
The MCP adapter shipped in slice 51 with one install option:
clone the repo and go build. This slice integrates pulse-mcp
into Pulse's existing governed release pipeline so a Pulse
release publishes a pulse-mcp binary alongside the unified agent
and the install scripts that bring it home in one command.
What ships:
- scripts/build-release.sh extended to build pulse-mcp for
the same multi-OS matrix as the unified agent, package
per-platform tarballs and zips, and copy bare binaries to
RELEASE_DIR for /releases/latest/download/ redirect
compatibility.
- .github/workflows/create-release.yml extended to upload
the bare pulse-mcp binaries plus install-mcp.sh and
install-mcp.ps1 as release assets.
- scripts/install-mcp.sh: bash one-line installer that
detects platform/arch, downloads the matching binary from
the configured release (latest by default), verifies SHA256
against the published checksums.txt, places at
~/.local/bin/pulse-mcp (or /usr/local/bin if not writable).
Honors PULSE_MCP_VERSION, PULSE_MCP_BIN_DIR, PULSE_MCP_REPO,
PULSE_MCP_NO_VERIFY env vars; declines Windows shells with
a pointer at the .ps1 sibling.
- scripts/install-mcp.ps1: PowerShell installer for Windows,
placing pulse-mcp.exe at $LOCALAPPDATA\pulse-mcp.
Documentation aligned:
- cmd/pulse-mcp/README.md gains an Install section above
Quick start with three options: one-line installer,
GitHub Release download, go install. Documents the macOS
Gatekeeper bypass since v1 is unnotarized by design.
- The Settings -> API Access agent-integrations panel now
surfaces the curl|bash command above the config snippet so
operators see "install pulse-mcp" before "configure your
MCP client."
- docs/releases/AGENT_PARADIGM.md drops the "no published
distribution path" item from "what it does not do yet" and
documents the Gatekeeper / Homebrew gaps as next-tier
follow-ups.
Trade-offs surfaced and chosen:
- Same cadence as Pulse: pulse-mcp ships per Pulse release,
not on its own track. The MCP server reads the manifest
from the Pulse it talks to, so version alignment is the
natural model.
- No Homebrew tap or core formula in v1. Maintaining a tap
is real ongoing work; foundation supports adding Homebrew
later as a layer.
- No Docker image. Stdio JSON-RPC fights Docker's stdin
/stdout pattern.
- No notarization in v1. SHA256 verification through the
installer preserves the audit trail; README documents the
Gatekeeper bypass.
Subsystem contract: deployment-installability.md gains
scripts/install-mcp.sh, scripts/install-mcp.ps1, and
cmd/pulse-mcp/ in canonical files (mid-list entries
renumbered) plus a paragraph documenting the new MCP entry
point alongside the existing installer family.
Verification artifacts:
- scripts/installtests/build_release_assets_test.go gains
TestBuildReleasePackagesPulseMcpForAllPlatforms which pins
the build/package/copy wiring and the load-bearing
install-mcp.sh helpers (platform detection, SHA256
verification, install-dir resolution).
- scripts/release_control/render_release_body_test.py gains
test_agent_paradigm_release_notes_blurb_documents_-
distribution_path which pins the AGENT_PARADIGM.md draft's
install-mcp.sh reference and the four-axis frame so a
future edit cannot regress the install story silently.
Smoke-tested install-mcp.sh locally on darwin-arm64: platform
detection, install-dir resolution, URL building, and 404 error
handling all correct. The full end-to-end install path becomes
live the moment a Pulse release ships pulse-mcp binaries; the
next RC cut will exercise it.
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.