mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-22 19:36:46 +00:00
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.
197 lines
8.9 KiB
Python
197 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Regression tests for publish-safe release body rendering."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import render_release_body
|
|
|
|
|
|
class RenderReleaseBodyTest(unittest.TestCase):
|
|
def test_sanitize_release_notes_strips_draft_markers_duplicate_sections_and_draft_links(self) -> None:
|
|
raw = """# Pulse v6.0.0-rc.2 Draft Release Notes
|
|
|
|
_Draft only. Do not treat this as published until the governed `v6.0.0-rc.2`
|
|
tag and GitHub prerelease exist._
|
|
|
|
Intro paragraph.
|
|
|
|
## Operator References
|
|
|
|
- `docs/releases/V6_RC2_OPERATOR_SUPPORT_PACK_DRAFT.md`
|
|
- `docs/UPGRADE_v6.md`
|
|
|
|
## Installation
|
|
|
|
Old install section.
|
|
|
|
## Promotion Metadata
|
|
|
|
Old metadata section.
|
|
"""
|
|
sanitized = render_release_body.sanitize_release_notes(raw, "6.0.0-rc.2")
|
|
self.assertIn("# Pulse v6.0.0-rc.2 Release Notes", sanitized)
|
|
self.assertNotIn("Draft Release Notes", sanitized)
|
|
self.assertNotIn("Draft only. Do not treat this as published", sanitized)
|
|
self.assertNotIn("_DRAFT.md", sanitized)
|
|
self.assertIn("- `docs/UPGRADE_v6.md`", sanitized)
|
|
self.assertNotIn("## Installation", sanitized)
|
|
self.assertNotIn("## Promotion Metadata", sanitized)
|
|
|
|
def test_main_renders_single_installation_and_promotion_metadata_sections(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
notes_file = Path(tmp) / "notes.md"
|
|
output_file = Path(tmp) / "body.md"
|
|
notes_file.write_text(
|
|
"# Pulse v6.0.0-rc.2 Draft Release Notes\n\n"
|
|
"_Draft only. Do not treat this as published until the governed `v6.0.0-rc.2` tag and GitHub prerelease exist._\n\n"
|
|
"Body.\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
args = render_release_body.parse_args.__wrapped__ if hasattr(render_release_body.parse_args, "__wrapped__") else None
|
|
del args # satisfy linters if wrapped implementation changes later
|
|
|
|
namespace = type(
|
|
"Args",
|
|
(),
|
|
{
|
|
"version": "6.0.0-rc.2",
|
|
"release_notes_file": str(notes_file),
|
|
"output": str(output_file),
|
|
"promotion_channel": "rc",
|
|
"candidate_tag": "v6.0.0-rc.2",
|
|
"promoted_prerelease_tag": "",
|
|
"rollback_target": "v5.1.28",
|
|
"rollback_command": "./scripts/install.sh --version v5.1.28",
|
|
"planned_ga_date": "",
|
|
"planned_v5_eos_date": "",
|
|
"hotfix_exception": "false",
|
|
"hotfix_reason": "",
|
|
},
|
|
)()
|
|
|
|
raw_text = Path(namespace.release_notes_file).read_text(encoding="utf-8")
|
|
sanitized = render_release_body.sanitize_release_notes(raw_text, namespace.version).rstrip("\n")
|
|
sections = [
|
|
sanitized,
|
|
render_release_body.build_installation_section(namespace.version),
|
|
render_release_body.build_promotion_metadata_section(namespace),
|
|
]
|
|
Path(namespace.output).write_text("\n\n".join(sections) + "\n", encoding="utf-8")
|
|
|
|
body = output_file.read_text(encoding="utf-8")
|
|
self.assertEqual(body.count("## Installation"), 1)
|
|
self.assertEqual(body.count("## Promotion Metadata"), 1)
|
|
self.assertIn("docker pull rcourtman/pulse:6.0.0-rc.2", body)
|
|
self.assertIn(
|
|
"public GitHub release assets and the public `rcourtman/pulse` Docker image are community builds",
|
|
body,
|
|
)
|
|
self.assertIn("https://pulserelay.pro/download.html", body)
|
|
self.assertIn("- Rollback target: v5.1.28", body)
|
|
|
|
def test_current_release_packets_use_pulse_mobile_handoff_copy(self) -> None:
|
|
repo_root = Path(__file__).resolve().parents[2]
|
|
packet_paths = (
|
|
"docs/releases/RELEASE_NOTES_v6.md",
|
|
"docs/releases/RELEASE_NOTES_v6_RC2_DRAFT.md",
|
|
"docs/releases/V6_CHANGELOG_RC2_DRAFT.md",
|
|
"docs/releases/V6_RC2_OPERATOR_SUPPORT_PACK_DRAFT.md",
|
|
"docs/releases/RELEASE_NOTES_v6_RC3_DRAFT.md",
|
|
"docs/releases/V6_CHANGELOG_RC3_DRAFT.md",
|
|
"docs/releases/V6_RC3_OPERATOR_SUPPORT_PACK_DRAFT.md",
|
|
"docs/releases/RELEASE_NOTES_v6_RC4_DRAFT.md",
|
|
"docs/releases/V6_CHANGELOG_RC4_DRAFT.md",
|
|
"docs/releases/V6_RC4_OPERATOR_SUPPORT_PACK_DRAFT.md",
|
|
)
|
|
|
|
for relative_path in packet_paths:
|
|
with self.subTest(relative_path=relative_path):
|
|
text = (repo_root / relative_path).read_text(encoding="utf-8")
|
|
self.assertIn("Pulse Mobile pairing for handoff", text)
|
|
self.assertNotIn("mobile app pairing", text)
|
|
self.assertNotIn("remote access/mobile/push", text)
|
|
|
|
def test_rc3_packet_records_commit_coverage_and_release_artifact_hardening(self) -> None:
|
|
repo_root = Path(__file__).resolve().parents[2]
|
|
release_notes = (repo_root / "docs/releases/RELEASE_NOTES_v6_RC3_DRAFT.md").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
changelog = (repo_root / "docs/releases/V6_CHANGELOG_RC3_DRAFT.md").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
support_pack = (
|
|
repo_root / "docs/releases/V6_RC3_OPERATOR_SUPPORT_PACK_DRAFT.md"
|
|
).read_text(encoding="utf-8")
|
|
|
|
self.assertIn("158d65ccdb81077c35b9237a1652b2774ddb5d5c", release_notes)
|
|
self.assertIn("commit count: `605`", changelog)
|
|
self.assertIn("broad hardening RC with a corrective maintenance core", changelog)
|
|
self.assertIn("Community-tier capabilities", release_notes)
|
|
self.assertIn("stable-channel release resolution", release_notes)
|
|
self.assertIn("Release asset uploads use bounded retries", release_notes)
|
|
self.assertIn(
|
|
"release artifact validation, draft metadata preservation, upload retries",
|
|
support_pack,
|
|
)
|
|
|
|
def test_rc4_packet_records_commit_coverage_and_identity_hardening(self) -> None:
|
|
repo_root = Path(__file__).resolve().parents[2]
|
|
release_notes = (repo_root / "docs/releases/RELEASE_NOTES_v6_RC4_DRAFT.md").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
changelog = (repo_root / "docs/releases/V6_CHANGELOG_RC4_DRAFT.md").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
support_pack = (
|
|
repo_root / "docs/releases/V6_RC4_OPERATOR_SUPPORT_PACK_DRAFT.md"
|
|
).read_text(encoding="utf-8")
|
|
|
|
self.assertIn("7cebe788590d0485f65bf4e04830356204657e86", release_notes)
|
|
self.assertIn("commit count: `57`", changelog)
|
|
self.assertIn("stable identity principals", support_pack)
|
|
self.assertIn("API-first action planning", changelog)
|
|
self.assertIn("monitored-system and child-resource volume unmetered", release_notes)
|
|
self.assertIn("Pulse Mobile pairing for handoff", support_pack)
|
|
self.assertIn("pin Docker install defaults to `6.0.0-rc.4`", changelog)
|
|
self.assertIn("Docker Compose and turnkey Docker installer defaults", release_notes)
|
|
self.assertIn("release-validation\ncommits", changelog)
|
|
self.assertIn("Tenant monitor state broadcasts", release_notes)
|
|
self.assertIn("tenant\nmonitor broadcast guard", changelog)
|
|
self.assertIn("live auth-env watcher teardown", release_notes)
|
|
self.assertIn("join live config watcher goroutines", changelog)
|
|
|
|
def test_agent_paradigm_release_notes_blurb_documents_distribution_path(self) -> None:
|
|
"""The agent-paradigm source draft must keep its honest scope:
|
|
an integrator reading the blurb sees a published distribution
|
|
path (the install-mcp script + GitHub Release binaries) when
|
|
the work lands, not the earlier "build from source" wording.
|
|
|
|
Pin the blurb's stable touchstones so a future edit that
|
|
accidentally regresses the install story (e.g. swaps the
|
|
one-line installer for "clone the repo" again) fails this
|
|
test instead of shipping silently into a release.
|
|
"""
|
|
repo_root = Path(__file__).resolve().parents[2]
|
|
blurb = (repo_root / "docs/releases/AGENT_PARADIGM.md").read_text(encoding="utf-8")
|
|
|
|
self.assertIn("install-mcp.sh", blurb, "blurb must reference the published install script")
|
|
self.assertIn("/api/agent/capabilities", blurb)
|
|
self.assertIn("cmd/pulse-mcp", blurb)
|
|
self.assertIn("cmd/agent-probe", blurb)
|
|
# The four-axis frame is the substrate's load-bearing claim;
|
|
# if any axis name drifts in the blurb, agents reading
|
|
# release notes will look for a different surface than what
|
|
# ships.
|
|
self.assertIn("Discovery", blurb)
|
|
self.assertIn("Read", blurb)
|
|
self.assertIn("Write", blurb)
|
|
self.assertIn("Push", blurb)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|