diff --git a/README.md b/README.md index c7ecc1579..732879e49 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,12 @@ Power-user shortcuts: ## ⚡ Quick Start +> **Paid Pulse Pro / Relay / legacy customers:** GitHub release assets and the +> public `rcourtman/pulse` Docker image are community builds. They can accept an +> activation key, but they do not include the private Pulse Pro runtime hooks. +> Use with your activation key to get the +> private Pulse Pro Docker image or Linux archive. + ### Option 1: Proxmox LXC (Recommended) Replace `vX.Y.Z` with the exact release tag you want, verify the signed installer, then run it on your Proxmox host: diff --git a/docs/DOCKER.md b/docs/DOCKER.md index 43c9bab5b..ba1a70347 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -2,6 +2,13 @@ Pulse is distributed as a lightweight, Alpine-based Docker image. +> **Paid Pulse Pro / Relay / legacy customers:** The public `rcourtman/pulse` +> Docker image is the community build. It can accept an activation key, but it +> does not include the private Pulse Pro runtime hooks. Use +> with your activation key, then run the +> private registry login and `PULSE_IMAGE=license.pulserelay.pro/pulse-pro:` +> compose commands shown there. + ## 🚀 Quick Start ```bash @@ -172,6 +179,12 @@ docker login registry.example.com The agent uses the Docker daemon's credentials for both pulling images and checking for updates. +Paid Pulse Pro Docker installs use the private Pulse Pro registry rather than +the public `rcourtman/pulse` image. Open , +paste your activation key, run the Docker login command shown there, then run +the shown `PULSE_IMAGE=license.pulserelay.pro/pulse-pro: docker compose pull` +and `docker compose up -d` commands from the host that already runs Pulse. + ### Disabling Update Features Pulse provides granular control over update features via environment variables on the **Pulse server**: diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 99fe4b9b4..18a50e97d 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -2,6 +2,12 @@ Pulse offers flexible installation options from Docker to enterprise-ready Kubernetes charts. +> **Paid Pulse Pro / Relay / legacy customers:** GitHub release assets and the +> public `rcourtman/pulse` Docker image are community builds. They can accept an +> activation key, but they do not include the private Pulse Pro runtime hooks. +> Use with your activation key to get the +> private Pulse Pro Docker image or Linux archive. + ## 🚀 Quick Start (Recommended) ### Proxmox VE (LXC installer) diff --git a/docs/UPGRADE_v6.md b/docs/UPGRADE_v6.md index 32136bfcd..0fe856236 100644 --- a/docs/UPGRADE_v6.md +++ b/docs/UPGRADE_v6.md @@ -140,6 +140,19 @@ activation, recovery, or BYOK/local AI setup instead; any exceptional support-issued entitlement is reflected through hosted entitlement state rather than a local in-app trial acquisition flow. +#### Paid Pulse Pro Runtime + +Paid Pulse Pro, Relay, and eligible legacy customers should not use public +GitHub release assets or the public `rcourtman/pulse` Docker image for paid +runtime features. Those public downloads are community builds. They can accept +an activation key, but they do not include the private Pulse Pro runtime hooks. + +Use with your activation key instead. +Docker users should run the private registry login and +`PULSE_IMAGE=license.pulserelay.pro/pulse-pro:` compose commands shown +there. Direct Linux users should download the private Pulse Pro archive from the +same page. + #### v5 License Migration Pulse v6 uses the activation/grant model for active licensing, but it can migrate valid Pulse v5 paid JWT-style licenses, including legacy Pro and Lifetime licenses. diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 191f23ba0..41764d698 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -701,6 +701,10 @@ blocked when the process is the public community build. Customer-facing paid runtime copy must send those users to the private Pulse Pro download rather than implying that the public GitHub release or public Docker image can unlock the same runtime hooks. +The owned self-hosted Plans surface must also show a direct Pulse Pro downloads +action when an active paid entitlement is running on the community runtime, and +the existing-key recovery panel must warn that paid Docker and Linux installs +need the private Pulse Pro image or archive before a customer pastes a key. That same browser-shell boundary also owns utility-nav suppression: `frontend-modern/src/AppLayout.tsx` must not expose a separate top-level Operations destination. Diagnostics, reports, and logs now live under the diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 846d7129c..e693a50de 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -219,9 +219,14 @@ server-side update execution surfaces. Paid-user GA is part of that same release boundary: the public Pulse release workflow builds OSS `pulse-v...` artifacts only, so release docs and runbooks must require a same-ref/same-version `pulse-enterprise` Pro package, verify - `pulse-pro-v...` archives identify `Pulse Pro (Enterprise)`, and keep the - paid install/upgrade path pointed at Pro artifacts or a verified paid image - before any paid-user Pro runtime claim is made. + `pulse-pro-v...` archives identify `Pulse Pro`, and keep the paid + install/upgrade path pointed at Pro artifacts or a verified paid image before + any paid-user Pro runtime claim is made. Public GitHub release assets and the + public `rcourtman/pulse` Docker image must be described as community builds + where paid customers are likely to install or upgrade, and generated public + release bodies must send Relay, Pulse Pro, and eligible legacy customers to + `https://pulserelay.pro/download.html` for the private Pulse Pro Docker image + or Linux archive. The repo-root VERSION file is part of the same governed boundary and must not drift as an unowned release-cut switch: changing the version string for a new RC or diff --git a/docs/releases/V6_PRERELEASE_RUNBOOK.md b/docs/releases/V6_PRERELEASE_RUNBOOK.md index d7bd80dac..2b26454f3 100644 --- a/docs/releases/V6_PRERELEASE_RUNBOOK.md +++ b/docs/releases/V6_PRERELEASE_RUNBOOK.md @@ -84,7 +84,7 @@ The workflow auto-marks `-rc.N`/`-alpha.N`/`-beta.N` as prerelease. other explicit trust-migration path before testing those newer packets. 6. For paid-user GA, run the `pulse-enterprise` Pro release workflow against the same Pulse ref/version, verify `pulse-pro-v...` archives exist, verify - `bin/pulse --version` identifies `Pulse Pro (Enterprise)`, and confirm the paid + `bin/pulse --version` identifies `Pulse Pro`, and confirm the paid install/upgrade docs point paid customers to the Pro artifacts or verified paid container image. diff --git a/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx b/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx index d371d3fb5..1a4703be9 100644 --- a/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx +++ b/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx @@ -28,6 +28,8 @@ interface ProLicensePlanSectionProps { tone: string; highlightsLabel: string; highlights: string[]; + actionLabel?: string; + actionUrl?: string; } | null; activationProof: { title: string; @@ -55,6 +57,10 @@ interface ProLicensePlanSectionProps { includedExtras: string[]; supplementalBadges: string[]; supplementalSummary?: string; + privateRuntimeAction?: { + actionLabel: string; + actionUrl: string; + }; }; entitlements: { in_grace_period?: boolean; @@ -109,6 +115,16 @@ export const ProLicensePlanSection: Component = (pro

{summary().title}

{summary().body}

+ + + {summary().actionLabel} + + 0}>

@@ -186,6 +202,18 @@ export const ProLicensePlanSection: Component = (pro {(summary) =>

{summary()}

} + + {(action) => ( + + {action().actionLabel} + + )} + 0}>

@@ -326,9 +354,7 @@ export const ProLicensePlanSection: Component = (pro - {getNoActiveSelfHostedActivationState().text} -

+
{getNoActiveSelfHostedActivationState().text}
} > @@ -343,9 +369,7 @@ export const ProLicensePlanSection: Component = (pro )}
-
- diff --git a/frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx b/frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx index 4cfc99fb6..f0e2aa232 100644 --- a/frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx +++ b/frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx @@ -67,6 +67,22 @@ export const SelfHostedCommercialRecoverySection: Component< {SELF_HOSTED_RECOVERY_PRESENTATION.helpTextAfterTerms}

+
+

+ {SELF_HOSTED_RECOVERY_PRESENTATION.privateRuntimeNotice.title} +

+

+ {SELF_HOSTED_RECOVERY_PRESENTATION.privateRuntimeNotice.body} +

+ + {SELF_HOSTED_RECOVERY_PRESENTATION.privateRuntimeNotice.actionLabel} + +

{SELF_HOSTED_RECOVERY_PRESENTATION.legacyNotice.title}

diff --git a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx index 8d167fc1e..cc6c49167 100644 --- a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx @@ -499,7 +499,7 @@ describe('ProLicensePanel', () => { expect(screen.getByText('Pulse Pro value proof')).toBeInTheDocument(); expect( screen.getByText( - "These checks come from this instance's entitlement payload, not from public pricing copy.", + "These checks come from this instance's entitlement and runtime payloads, not from public pricing copy.", ), ).toBeInTheDocument(); expect(screen.getByText('Remote access, pairing, and push')).toBeInTheDocument(); @@ -1118,7 +1118,9 @@ describe('ProLicensePanel', () => { expect(proLicensePlanSectionSource).not.toContain('getTrialEndedProLicenseNotice'); expect(proLicensePlanSectionSource).toContain('currentPlanSummary.title'); expect(proLicensePlanSectionSource).toContain('currentPlanSummary.supplementalBadges'); + expect(proLicensePlanSectionSource).toContain('currentPlanSummary.privateRuntimeAction'); expect(proLicensePlanSectionSource).toContain('props.activationSuccessSummary'); + expect(proLicensePlanSectionSource).toContain('summary().actionUrl'); expect(proLicensePlanSectionSource).toContain('props.planComparisonSummary.cards.length > 0'); expect(proLicensePlanSectionSource).toContain( 'SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonSectionTitle', @@ -1145,6 +1147,7 @@ describe('ProLicensePanel', () => { ); expect(selfHostedCommercialRecoverySectionSource).toContain('TERMS_DOC_URL'); expect(selfHostedCommercialRecoverySectionSource).toContain('disclosureLabel'); + expect(selfHostedCommercialRecoverySectionSource).toContain('privateRuntimeNotice'); expect(selfHostedCommercialRecoverySectionSource).toContain('recoverySectionTitle'); expect(selfHostedCommercialRecoverySectionSource).not.toContain( 'https://github.com/rcourtman/Pulse/blob/main/TERMS.md', diff --git a/frontend-modern/src/utils/__tests__/licensePresentation.test.ts b/frontend-modern/src/utils/__tests__/licensePresentation.test.ts index e95619234..53ba6b6c0 100644 --- a/frontend-modern/src/utils/__tests__/licensePresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/licensePresentation.test.ts @@ -53,6 +53,11 @@ describe('licensePresentation', () => { fieldLabel: 'License or Activation Key', activateIdleLabel: 'Activate Key', clearIdleLabel: 'Clear Key', + privateRuntimeNotice: { + title: 'Paid Docker and Linux installs use a private runtime', + actionLabel: 'Open Pulse Pro downloads', + actionUrl: 'https://pulserelay.pro/download.html', + }, legacyNotice: { title: 'Legacy v5 license detected', }, @@ -369,6 +374,10 @@ describe('licensePresentation', () => { title: 'Current plan: Pulse Pro', body: expect.stringContaining('private Pulse Pro runtime'), supplementalBadges: ['Pro runtime missing'], + privateRuntimeAction: { + actionLabel: 'Open Pulse Pro downloads', + actionUrl: 'https://pulserelay.pro/download.html', + }, }); expect(getSelfHostedActivationProofPresentation(entitlements)?.items[0]).toMatchObject({ label: 'Pulse Pro runtime', @@ -387,6 +396,8 @@ describe('licensePresentation', () => { tone: expect.stringContaining('amber'), body: expect.stringContaining('community runtime'), highlightsLabel: 'Licensed capabilities', + actionLabel: 'Open Pulse Pro downloads', + actionUrl: 'https://pulserelay.pro/download.html', }); }); diff --git a/frontend-modern/src/utils/licensePresentation.ts b/frontend-modern/src/utils/licensePresentation.ts index 659aa9b40..e573a54f7 100644 --- a/frontend-modern/src/utils/licensePresentation.ts +++ b/frontend-modern/src/utils/licensePresentation.ts @@ -74,6 +74,11 @@ export interface SelfHostedActivationNoticeCopy { body: string; } +export interface LicenseExternalActionCopy { + actionLabel: string; + actionUrl: string; +} + export interface SelfHostedRecoveryPresentation { disclosureLabel: string; disclosureDescription: string; @@ -82,6 +87,7 @@ export interface SelfHostedRecoveryPresentation { helpTextBeforeTerms: string; helpTextAfterTerms: string; termsLabel: string; + privateRuntimeNotice: SelfHostedActivationNoticeCopy & LicenseExternalActionCopy; activateIdleLabel: string; activatePendingLabel: string; clearIdleLabel: string; @@ -98,6 +104,7 @@ export interface SelfHostedCurrentPlanPresentation { includedExtras: string[]; supplementalBadges: string[]; supplementalSummary?: string; + privateRuntimeAction?: LicenseExternalActionCopy; } export interface SelfHostedActivationProofItem { @@ -116,6 +123,8 @@ export interface SelfHostedActivationProofPresentation { export interface SelfHostedActivationSuccessPresentation extends LicenseInlineNotice { highlightsLabel: string; highlights: string[]; + actionLabel?: string; + actionUrl?: string; } export type SelfHostedActivationSuccessSource = 'manual' | 'purchase'; @@ -156,6 +165,8 @@ const PRO_RUNTIME_REQUIRED_TIERS = new Set([ 'enterprise', ]); +export const PULSE_PRO_DOWNLOAD_URL = 'https://pulserelay.pro/download.html'; + const isActivePaidRuntimeState = (subscriptionState?: string | null): boolean => { const normalized = (subscriptionState || '').trim().toLowerCase(); return normalized === 'active' || normalized === 'grace' || normalized === 'trial'; @@ -436,13 +447,13 @@ export const getSelfHostedCurrentPlanPresentation = ({ if (runtimeMismatch) { supplementalBadges.push('Pro runtime missing'); supplementalDetails.unshift( - 'Public GitHub releases and the public Docker image are community builds. Install the private Pulse Pro runtime to test Pro-only runtime hooks during the trial.', + 'Public GitHub releases and the public Docker image are community builds. Open Pulse Pro downloads with your activation key to install the private Pulse Pro runtime and test Pro-only runtime hooks during the trial.', ); } return { title: `Current plan: ${planLabel} Trial`, body: runtimeMismatch - ? `${planLabel} trial entitlement is active, but this install is running the community runtime. Install the private Pulse Pro runtime from the paid download page to use Pro-only features.` + ? `${planLabel} trial entitlement is active, but this install is running the community runtime. Open Pulse Pro downloads with your activation key and install the private Pulse Pro runtime to use Pro-only features.` : unlockedFeatures.length > 0 ? `${planLabel} trial capabilities are active on this instance right now.` : `${planLabel} trial entitlement is being confirmed for this instance.`, @@ -452,6 +463,14 @@ export const getSelfHostedCurrentPlanPresentation = ({ includedExtras, supplementalBadges, supplementalSummary: supplementalDetails.join(' '), + ...(runtimeMismatch + ? { + privateRuntimeAction: { + actionLabel: 'Open Pulse Pro downloads', + actionUrl: PULSE_PRO_DOWNLOAD_URL, + }, + } + : {}), }; } @@ -477,13 +496,17 @@ export const getSelfHostedCurrentPlanPresentation = ({ ); return { title: `Current plan: ${planLabel}`, - body: `${planLabel} is active, but this install is running the community runtime. Install the private Pulse Pro runtime from the paid download page to use Pro-only features such as Audit Log, Audit Webhooks, RBAC, and governed remediation.`, + body: `${planLabel} is active, but this install is running the community runtime. Open Pulse Pro downloads with your activation key and install the private Pulse Pro runtime to use Pro-only features such as Audit Log, Audit Webhooks, RBAC, and governed remediation.`, unlockedFeaturesLabel, unlockedFeatures, includedExtrasLabel: includedExtras.length > 0 ? 'Included extras' : undefined, includedExtras, supplementalBadges, supplementalSummary: supplementalDetails.join(' '), + privateRuntimeAction: { + actionLabel: 'Open Pulse Pro downloads', + actionUrl: PULSE_PRO_DOWNLOAD_URL, + }, }; } @@ -588,7 +611,7 @@ export const getSelfHostedActivationProofPresentation = ( statusLabel: hasProRuntime ? 'Active' : 'Needs attention', detail: hasProRuntime ? 'This install reports the private Pulse Pro runtime.' - : 'This install reports the community runtime. Install the private Pulse Pro runtime from the paid download page; public GitHub releases and the public Docker image do not include Pro-only runtime hooks.', + : `This install reports the community runtime. Open ${PULSE_PRO_DOWNLOAD_URL} with your activation key and install the private Pulse Pro runtime; public GitHub releases and the public Docker image do not include Pro-only runtime hooks.`, }); } items.push( @@ -698,13 +721,19 @@ export const getSelfHostedActivationSuccessPresentation = ({ body: source === 'purchase' ? runtimeMismatch - ? `Checkout completed and the license is active. This install is still running the community runtime, so install the private Pulse Pro runtime from the paid download page before using Pro-only features.` + ? `Checkout completed and the license is active. This install is still running the community runtime, so open Pulse Pro downloads with your activation key and install the private Pulse Pro runtime before using Pro-only features.` : `Checkout completed and this instance is now running ${planLabel}.` : runtimeMismatch - ? `The activation key was accepted. This install is still running the community runtime, so install the private Pulse Pro runtime from the paid download page before using Pro-only features.` + ? `The activation key was accepted. This install is still running the community runtime, so open Pulse Pro downloads with your activation key and install the private Pulse Pro runtime before using Pro-only features.` : `The activation key was accepted and this instance is now running ${planLabel}.`, highlightsLabel: runtimeMismatch ? 'Licensed capabilities' : 'Available now on this instance', highlights, + ...(runtimeMismatch + ? { + actionLabel: 'Open Pulse Pro downloads', + actionUrl: PULSE_PRO_DOWNLOAD_URL, + } + : {}), }; }; @@ -886,6 +915,12 @@ export const SELF_HOSTED_RECOVERY_PRESENTATION: SelfHostedRecoveryPresentation = 'Paste the Pulse v6 activation key shown on the hosted checkout success page. A backup copy is also sent by email, but the hosted success page is the primary handoff. You can also paste a legacy Pulse v5 Pro/Lifetime license key and Pulse will exchange it automatically during activation when migration is available. By activating a license, you agree to the', helpTextAfterTerms: '.', termsLabel: 'Terms of Service', + privateRuntimeNotice: { + title: 'Paid Docker and Linux installs use a private runtime', + body: 'Public GitHub releases and the public Docker image are community builds. They can accept an activation key, but Pro-only runtime hooks require the private Pulse Pro Docker image or Linux archive.', + actionLabel: 'Open Pulse Pro downloads', + actionUrl: PULSE_PRO_DOWNLOAD_URL, + }, activateIdleLabel: 'Activate Key', activatePendingLabel: 'Activating...', clearIdleLabel: 'Clear Key', diff --git a/scripts/generate-release-notes.sh b/scripts/generate-release-notes.sh index 65ee4447e..7abdcae98 100755 --- a/scripts/generate-release-notes.sh +++ b/scripts/generate-release-notes.sh @@ -216,6 +216,12 @@ helm upgrade --install pulse oci://ghcr.io/rcourtman/pulse-chart --version ${VER See the [Installation Guide](https://github.com/rcourtman/Pulse#installation) for details. +Paid Pulse Pro, Relay, and eligible legacy customers: public GitHub release +assets and the public \`rcourtman/pulse\` Docker image are community builds. They do +not include the private Pulse Pro runtime hooks. Use +https://pulserelay.pro/download.html with your activation key to get the +private Pulse Pro Docker image or Linux archive. + GUIDELINES: - Write plain, factual release notes. No marketing language or excitement. - Only mention features that exist in the FINAL code state diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 2ecca984a..af082e4b7 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -349,13 +349,14 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("It does not automatically check out or build `pulse-enterprise`.", runbook) self.assertIn("public `pulse-v...` release archives are OSS runtime artifacts", runbook) self.assertIn("`pulse-pro-v...` archives", runbook) - self.assertIn("`bin/pulse --version` identifies `Pulse Pro (Enterprise)`", runbook) + self.assertIn("`bin/pulse --version` identifies `Pulse Pro`", runbook) self.assertIn("Paid-user GA is part of that same release boundary", contract) self.assertIn( "the public Pulse release workflow builds OSS `pulse-v...` artifacts only", normalize_ws(contract), ) - self.assertIn("`pulse-pro-v...` archives identify `Pulse Pro (Enterprise)`", contract) + self.assertIn("`pulse-pro-v...` archives identify `Pulse Pro`", contract) + self.assertIn("https://pulserelay.pro/download.html", contract) self.assertIn("control_plane.py --branch-for-version", content) self.assertIn('git fetch --prune origin main "${REQUIRED_BRANCH}" --tags', content) self.assertIn('REQUIRED_BRANCH: ${{ steps.branch_policy.outputs.required_branch }}', content) diff --git a/scripts/release_control/render_release_body.py b/scripts/release_control/render_release_body.py index 680bd71e7..3d26f8f06 100644 --- a/scripts/release_control/render_release_body.py +++ b/scripts/release_control/render_release_body.py @@ -80,6 +80,8 @@ def build_installation_section(version: str) -> str: f"Update your `docker-compose.yml` to use `rcourtman/pulse:{version}`", "", "See the [Installation Guide](https://github.com/rcourtman/Pulse#installation) for complete setup instructions.", + "", + "Paid Pulse Pro, Relay, and eligible legacy customers: public GitHub release assets and the public `rcourtman/pulse` Docker image are community builds. They do not include the private Pulse Pro runtime hooks. Use https://pulserelay.pro/download.html with your activation key to get the private Pulse Pro Docker image or Linux archive.", ] ) diff --git a/scripts/release_control/render_release_body_test.py b/scripts/release_control/render_release_body_test.py index 22f4a3a72..3d55873a8 100644 --- a/scripts/release_control/render_release_body_test.py +++ b/scripts/release_control/render_release_body_test.py @@ -87,6 +87,11 @@ Old metadata section. 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: