From 96c2e160c9e4968e1fbb3a91a1c0da1a917536e6 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 5 May 2026 15:59:23 +0100 Subject: [PATCH] Fix RC4 release validation blockers --- docker-compose.yml | 2 +- ...legacy-cleanup-v6-rc4-packet-2026-05-05.md | 36 +++++++++++-- ...-promotion-readiness-blocked-2026-04-04.md | 15 +++--- .../subsystems/deployment-installability.md | 7 +++ docs/releases/RELEASE_NOTES_v6_RC4_DRAFT.md | 11 ++-- docs/releases/V6_CHANGELOG_RC4_DRAFT.md | 8 +-- internal/config/billing_state_test.go | 12 ++--- scripts/install-docker.sh | 2 +- .../installtests/install_docker_sh_test.go | 13 +++++ .../render_release_body_test.py | 3 ++ tests/migration/v5_full_upgrade_test.go | 52 +++++++++---------- .../v5_real_exchange_upgrade_test.go | 22 ++++---- 12 files changed, 118 insertions(+), 65 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7a2df2a05..c79b722b7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: pulse: - image: ${PULSE_IMAGE:-rcourtman/pulse:6.0.0-rc.3} + image: ${PULSE_IMAGE:-rcourtman/pulse:6.0.0-rc.4} container_name: pulse restart: unless-stopped logging: diff --git a/docs/release-control/v6/internal/records/documentation-currentness-and-legacy-cleanup-v6-rc4-packet-2026-05-05.md b/docs/release-control/v6/internal/records/documentation-currentness-and-legacy-cleanup-v6-rc4-packet-2026-05-05.md index 30f132d8c..1447b25b7 100644 --- a/docs/release-control/v6/internal/records/documentation-currentness-and-legacy-cleanup-v6-rc4-packet-2026-05-05.md +++ b/docs/release-control/v6/internal/records/documentation-currentness-and-legacy-cleanup-v6-rc4-packet-2026-05-05.md @@ -93,13 +93,36 @@ The current RC packet now points at `v6.0.0-rc.4`: - `docs/release-control/v6/internal/subsystems/deployment-installability.md` - records that RC4 follows the later-corrective-RC packet requirements for rollback, trust-root continuity, and release-control evidence +- `docker-compose.yml` + - pins the default Pulse image tag to `6.0.0-rc.4` +- `scripts/install-docker.sh` + - pins the standalone installer fallback image version to `6.0.0-rc.4` +- `internal/config/billing_state_test.go` + - asserts that billing-state normalization scrubs retired monitored-system + limit metadata instead of preserving it as entitlement state +- `tests/migration/v5_full_upgrade_test.go` and + `tests/migration/v5_real_exchange_upgrade_test.go` + - align v5-to-v6 exchange proof with the canonical self-hosted contract: + migrated self-hosted plans preserve plan identity and paid continuity + without reintroducing monitored-system caps ## Outcome -The audit did not identify a new unhandled code blocker from the commit range. -It did identify a release-packet currentness gap because the repo still pointed -operators at `rc.3` while the branch had moved beyond the published `rc.3` -tag. The gap is addressed by the RC4 packet before release workflow dispatch. +The audit did not identify a new unhandled code blocker from the feature/runtime +commit range. It did identify a release-packet currentness gap because the repo +still pointed operators at `rc.3` while the branch had moved beyond the +published `rc.3` tag. The initial draft release workflow also exposed two +release-validation gaps before publication: + +- Docker Compose and turnkey Docker install defaults still pinned the RC3 image + tag. +- Backend migration and billing-state tests still expected the retired + `max_monitored_systems` contract to survive in self-hosted entitlement state. + +Both gaps are addressed before publishing RC4. The corrected tests assert the +canonical root contract: self-hosted v6 plans preserve paid continuity without +monitored-system volume caps, and `max_monitored_systems` remains only as +legacy metadata to scrub. No public issue comment, retitle, closure, or customer-facing message was made as part of this packet update. @@ -112,3 +135,8 @@ as part of this packet update. - `PYTHONPATH=scripts/release_control python3 -m unittest scripts.release_control.release_promotion_policy_test.ReleasePromotionPolicyTest.test_release_notes_index_points_at_current_rc_packet scripts.release_control.release_promotion_policy_test.ReleasePromotionPolicyTest.test_operator_support_packs_keep_free_first_paid_continuity_wording scripts.release_control.release_promotion_policy_test.ReleasePromotionPolicyTest.test_version_file_matches_current_rc_packet scripts.release_control.release_promotion_policy_test.ReleasePromotionPolicyTest.test_upgrade_guide_points_at_current_rc_support_pack -q` - `python3 scripts/release_control/resolve_release_promotion.py --version 6.0.0-rc.4 --rollback-version v5.1.29 --release-notes-file docs/releases/RELEASE_NOTES_v6_RC4_DRAFT.md` - `python3 scripts/release_control/status_audit.py --pretty` +- Draft release workflow `25382802275`: + - `prepare`, `frontend_checks`, `helm_smoke`, and `docker_build` passed + - `backend_tests` failed on stale RC3 Docker defaults and stale retired + monitored-system cap expectations +- `go test -race ./internal/config ./tests/migration ./scripts/installtests` diff --git a/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md b/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md index e6a952747..5cfa3f71b 100644 --- a/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md +++ b/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md @@ -8,25 +8,22 @@ 1. The latest shipped Pulse v6 prerelease tag is `v6.0.0-rc.3`. 2. That shipped prerelease tag resolves to commit `f1744d36d0bde3c8735ae75a190af45c35087841`. -3. The selected remote ref `origin/pulse/v6-release` is still behind the current - local governed branch state, so `Release Dry Run` would exercise stale remote - control-plane metadata instead of the intended candidate. -4. The governed release profile in `docs/release-control/control_plane.json` +3. The governed release profile in `docs/release-control/control_plane.json` currently declares both `prerelease_branch` and `stable_branch` as `pulse/v6-release`. -5. The active control-plane target is still `v6-product-lane-expansion`, not +4. The active control-plane target is still `v6-product-lane-expansion`, not `v6-ga-promotion`. -6. The active local `pulse/v6-release` branch currently reports `VERSION=6.0.0-rc.4`, so the +5. The active local `pulse/v6-release` branch currently reports `VERSION=6.0.0-rc.4`, so the working line is still prerelease and there is not yet a governed local stable `6.0.0` candidate. -7. There is still no governed `Prerelease-to-GA Rehearsal Record` proving a successful +6. There is still no governed `Prerelease-to-GA Rehearsal Record` proving a successful non-publish `Release Dry Run` for the eventual stable `6.0.0` candidate. -8. `docs/releases/RELEASE_NOTES_v6.md` and +7. `docs/releases/RELEASE_NOTES_v6.md` and `docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md` now carry the currently proposed exact dates for the eventual GA notice: - `v6` GA date: `2026-04-20` - `v5` end-of-support date: `2026-07-19` -9. There is still no governed `Release Dry Run` artifact or rehearsal record +8. There is still no governed `Release Dry Run` artifact or rehearsal record exercising stable inputs for: - `version=6.0.0` - `promoted_from_tag=v6.0.0-rc.3` diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index f1291db3c..d1d604796 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -367,6 +367,13 @@ root `docker-compose.yml` sample and `scripts/install-docker.sh` must default to the governed `VERSION` cut instead of floating `:latest`, so self-hosted operators only move to a newer image when they choose a newer explicit tag or override `PULSE_IMAGE`. +For every RC or stable release cut, those Docker defaults must move with the +same governed `VERSION` change and the installer proof in +`scripts/installtests/install_docker_sh_test.go` must assert both the repo-root +compose image default and the standalone installer fallback constant. A draft +release workflow failure caused by stale Docker image pins is a release-packet +blocker until the defaults, tests, and evidence record are refreshed from the +new branch head. `internal/updates/` is the live deployment and upgrade planner. It owns deployment-type detection, update-plan generation, adapter selection, server diff --git a/docs/releases/RELEASE_NOTES_v6_RC4_DRAFT.md b/docs/releases/RELEASE_NOTES_v6_RC4_DRAFT.md index dab928fb9..d4f03f11e 100644 --- a/docs/releases/RELEASE_NOTES_v6_RC4_DRAFT.md +++ b/docs/releases/RELEASE_NOTES_v6_RC4_DRAFT.md @@ -24,10 +24,13 @@ hardening into a retestable v6 candidate: - Workloads empty-state detection, Patrol mobile header controls, mock-mode legacy sidecar cleanup, and agent-security guidance were refreshed -This packet was audited against all `51` commits in the exact `rc.3` to `rc.4` -candidate range, from the published `v6.0.0-rc.3` tag commit +This packet was audited against all `51` feature and runtime commits in the +exact `rc.3` to `rc.4` candidate range, from the published `v6.0.0-rc.3` tag commit `f1744d36d0bde3c8735ae75a190af45c35087841` through candidate commit -`3f16d7845a92d6bf0c5700728bd70e1f4fe32966`. +`3f16d7845a92d6bf0c5700728bd70e1f4fe32966`. The final prerelease target also +includes RC4 packet and release-validation commits that set the governed +version, pin Docker install defaults to `6.0.0-rc.4`, and align migration tests +with the canonical self-hosted licensing contract. ## Support Stance @@ -97,6 +100,8 @@ candidate range, from the published `v6.0.0-rc.3` tag commit - The Agent Security documentation entry now points operators at the current privilege guidance without leaving a stale support-pack reference. - Public demo admin reads stay hidden from the demo surface. +- Docker Compose and turnkey Docker installer defaults now pin the RC4 image + tag instead of the historical RC3 tag. ## What Existing v5 Users Should Re-Test In `rc.4` diff --git a/docs/releases/V6_CHANGELOG_RC4_DRAFT.md b/docs/releases/V6_CHANGELOG_RC4_DRAFT.md index e3630aa34..01476930e 100644 --- a/docs/releases/V6_CHANGELOG_RC4_DRAFT.md +++ b/docs/releases/V6_CHANGELOG_RC4_DRAFT.md @@ -21,8 +21,8 @@ match the current governed v6 architecture before wider RC retesting. ## Commit Coverage Audit -The changelog was audited against every commit in the exact release range for -the current candidate head: +The changelog was audited against every feature/runtime commit in the exact +release range for the current candidate head: - `v6.0.0-rc.3`: `f1744d36d0bde3c8735ae75a190af45c35087841` - candidate commit: `3f16d7845a92d6bf0c5700728bd70e1f4fe32966` @@ -36,7 +36,9 @@ cleanup, API-first action planning, CLI action and fleet reads, action audit execution proof, self-hosted licensing continuity, root-agent and Proxmox setup hardening, TrueNAS/RAID/Ceph/storage correctness, Workloads empty-state handling, Patrol mobile controls, mock-mode cleanup, and release-control -evidence. +evidence. The final RC4 prerelease target also includes packet and +release-validation commits that pin Docker install defaults to `6.0.0-rc.4` and +remove stale migration-test expectations for retired monitored-system caps. ## Major Changes diff --git a/internal/config/billing_state_test.go b/internal/config/billing_state_test.go index 55c1b9731..090b2cbd9 100644 --- a/internal/config/billing_state_test.go +++ b/internal/config/billing_state_test.go @@ -366,9 +366,8 @@ func TestBillingState_SaveCanonicalizesCloudPlanContract(t *testing.T) { require.NoError(t, err) require.NotNil(t, loaded) assert.Equal(t, "cloud_starter", loaded.PlanVersion) - assert.Equal(t, int64(10), loaded.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey]) - _, hasOld := loaded.Limits["max_nodes"] - assert.False(t, hasOld) + assert.NotContains(t, loaded.Limits, pkglicensing.MaxMonitoredSystemsLicenseGateKey) + assert.NotContains(t, loaded.Limits, "max_nodes") data, err := os.ReadFile(filepath.Join(dir, "billing.json")) require.NoError(t, err) @@ -378,9 +377,8 @@ func TestBillingState_SaveCanonicalizesCloudPlanContract(t *testing.T) { assert.Equal(t, "cloud_starter", raw["plan_version"]) rawLimits, ok := raw["limits"].(map[string]any) require.True(t, ok) - assert.Equal(t, float64(10), rawLimits[pkglicensing.MaxMonitoredSystemsLicenseGateKey]) - _, hasOld = rawLimits["max_nodes"] - assert.False(t, hasOld) + assert.NotContains(t, rawLimits, pkglicensing.MaxMonitoredSystemsLicenseGateKey) + assert.NotContains(t, rawLimits, "max_nodes") } func TestBillingState_GrandfatheredRecurringSelfHostedPlanStaysUncapped(t *testing.T) { @@ -487,7 +485,7 @@ func TestBillingState_AllFieldsSurviveRoundTrip(t *testing.T) { // Every field must survive save → reload → HMAC verify. assert.ElementsMatch(t, []string{"relay", "ai_autofix"}, loaded.Capabilities) - assert.Equal(t, map[string]int64{pkglicensing.MaxMonitoredSystemsLicenseGateKey: 50, "max_hosts": 100}, loaded.Limits) + assert.Equal(t, map[string]int64{"max_hosts": 100}, loaded.Limits) assert.ElementsMatch(t, []string{"active_agents", "api_calls"}, loaded.MetersEnabled) assert.Equal(t, "pro-v2", loaded.PlanVersion) assert.Equal(t, entitlements.SubStateActive, loaded.SubscriptionState) diff --git a/scripts/install-docker.sh b/scripts/install-docker.sh index 64c833691..6c026af8c 100755 --- a/scripts/install-docker.sh +++ b/scripts/install-docker.sh @@ -6,7 +6,7 @@ set -euo pipefail SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" DOCKER_IMAGE_REPO="${DOCKER_IMAGE_REPO:-rcourtman/pulse}" -CANONICAL_DEFAULT_PULSE_VERSION="6.0.0-rc.3" +CANONICAL_DEFAULT_PULSE_VERSION="6.0.0-rc.4" resolve_default_pulse_version() { if [ -n "${PULSE_IMAGE_VERSION:-}" ]; then diff --git a/scripts/installtests/install_docker_sh_test.go b/scripts/installtests/install_docker_sh_test.go index 11829f160..202717903 100644 --- a/scripts/installtests/install_docker_sh_test.go +++ b/scripts/installtests/install_docker_sh_test.go @@ -97,6 +97,19 @@ func TestRepoDockerComposeDefaultPinsCurrentVersion(t *testing.T) { } } +func TestInstallDockerScriptFallbackPinsCurrentVersion(t *testing.T) { + version := currentReleaseVersion(t) + content, err := os.ReadFile(repoFile("scripts", "install-docker.sh")) + if err != nil { + t.Fatalf("read install-docker.sh: %v", err) + } + + text := string(content) + if !strings.Contains(text, `CANONICAL_DEFAULT_PULSE_VERSION="`+version+`"`) { + t.Fatalf("install-docker.sh fallback must pin the current release version:\n%s", text) + } +} + func runInstallDockerScript(t *testing.T, workDir string, envVars ...string) { t.Helper() diff --git a/scripts/release_control/render_release_body_test.py b/scripts/release_control/render_release_body_test.py index d5f85be11..f08b7f062 100644 --- a/scripts/release_control/render_release_body_test.py +++ b/scripts/release_control/render_release_body_test.py @@ -152,6 +152,9 @@ Old metadata section. 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 commits", changelog) if __name__ == "__main__": diff --git a/tests/migration/v5_full_upgrade_test.go b/tests/migration/v5_full_upgrade_test.go index f4e19be7f..f369104fd 100644 --- a/tests/migration/v5_full_upgrade_test.go +++ b/tests/migration/v5_full_upgrade_test.go @@ -448,15 +448,14 @@ func TestV5FullUpgradeScenario(t *testing.T) { require.NoError(t, persistence.Save(legacyLicense)) grantJWT, grantPublicKey, err := pkglicensing.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{ - LicenseID: "lic_v5_migrated", - Tier: string(pkglicensing.TierLifetime), - PlanKey: "v5_lifetime_grandfathered", - State: "active", - Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierLifetime]...), - MaxMonitoredSystems: pkglicensing.TierMonitoredSystemLimits[pkglicensing.TierLifetime], - IssuedAt: time.Now().Unix(), - ExpiresAt: time.Now().Add(72 * time.Hour).Unix(), - Email: "legacy-lifetime@example.com", + LicenseID: "lic_v5_migrated", + Tier: string(pkglicensing.TierLifetime), + PlanKey: "v5_lifetime_grandfathered", + State: "active", + Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierLifetime]...), + IssuedAt: time.Now().Unix(), + ExpiresAt: time.Now().Add(72 * time.Hour).Unix(), + Email: "legacy-lifetime@example.com", }) require.NoError(t, err) pkglicensing.SetPublicKey(grantPublicKey) @@ -472,11 +471,10 @@ func TestV5FullUpgradeScenario(t *testing.T) { w.WriteHeader(http.StatusCreated) require.NoError(t, json.NewEncoder(w).Encode(pkglicensing.ActivateInstallationResponse{ License: pkglicensing.ActivateResponseLicense{ - LicenseID: "lic_v5_migrated", - State: "active", - Tier: string(pkglicensing.TierLifetime), - Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierLifetime]...), - MaxMonitoredSystems: pkglicensing.TierMonitoredSystemLimits[pkglicensing.TierLifetime], + LicenseID: "lic_v5_migrated", + State: "active", + Tier: string(pkglicensing.TierLifetime), + Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierLifetime]...), }, Installation: pkglicensing.ActivateResponseInstallation{ InstallationID: "inst_v5_migrated", @@ -587,15 +585,14 @@ func TestV5FullUpgradeScenario(t *testing.T) { require.NoError(t, persistence.Save(legacyLicense)) grantJWT, grantPublicKey, err := pkglicensing.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{ - LicenseID: tc.licenseID, - Tier: string(pkglicensing.TierPro), - PlanKey: tc.planKey, - State: "active", - Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...), - MaxMonitoredSystems: pkglicensing.TierMonitoredSystemLimits[pkglicensing.TierPro], - IssuedAt: time.Now().Unix(), - ExpiresAt: time.Now().Add(72 * time.Hour).Unix(), - Email: tc.email, + LicenseID: tc.licenseID, + Tier: string(pkglicensing.TierPro), + PlanKey: tc.planKey, + State: "active", + Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...), + IssuedAt: time.Now().Unix(), + ExpiresAt: time.Now().Add(72 * time.Hour).Unix(), + Email: tc.email, }) require.NoError(t, err) pkglicensing.SetPublicKey(grantPublicKey) @@ -611,11 +608,10 @@ func TestV5FullUpgradeScenario(t *testing.T) { w.WriteHeader(http.StatusCreated) require.NoError(t, json.NewEncoder(w).Encode(pkglicensing.ActivateInstallationResponse{ License: pkglicensing.ActivateResponseLicense{ - LicenseID: tc.licenseID, - State: "active", - Tier: string(pkglicensing.TierPro), - Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...), - MaxMonitoredSystems: pkglicensing.TierMonitoredSystemLimits[pkglicensing.TierPro], + LicenseID: tc.licenseID, + State: "active", + Tier: string(pkglicensing.TierPro), + Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...), }, Installation: pkglicensing.ActivateResponseInstallation{ InstallationID: tc.installID, diff --git a/tests/migration/v5_real_exchange_upgrade_test.go b/tests/migration/v5_real_exchange_upgrade_test.go index b5d24e6fc..574c30e3b 100644 --- a/tests/migration/v5_real_exchange_upgrade_test.go +++ b/tests/migration/v5_real_exchange_upgrade_test.go @@ -128,7 +128,7 @@ func TestV5PaidLicenseUpgrade_RealLicenseServerExchange(t *testing.T) { assert.NotEqual(t, tc.licenseID, current.Claims.LicenseID, "real exchange should promote the legacy token into a new canonical v6 license id") assert.Equal(t, tc.tier, current.Claims.Tier) assert.Equal(t, tc.planKey, current.Claims.PlanVersion) - assert.Equal(t, pkglicensing.TierMonitoredSystemLimits[tc.tier], current.Claims.MaxMonitoredSystems) + assertMonitoredSystemLimitAbsent(t, current.Claims.EffectiveLimits()) activationState, err := persistence.LoadActivationState() require.NoError(t, err) @@ -154,7 +154,6 @@ func TestV5PaidLicenseUpgrade_RealLicenseServerExchange(t *testing.T) { assert.Equal(t, tc.tier, status.Tier) assert.Equal(t, tc.planKey, status.PlanVersion) assert.Equal(t, tc.wantIsLifetime, status.IsLifetime) - assert.Equal(t, pkglicensing.TierMonitoredSystemLimits[tc.tier], status.MaxMonitoredSystems) entReq := httpRequestWithOrg(http.MethodGet, "/api/license/entitlements", ctx) entRec := responseRecorder() @@ -167,7 +166,7 @@ func TestV5PaidLicenseUpgrade_RealLicenseServerExchange(t *testing.T) { assert.Equal(t, "active", entitlements.SubscriptionState) assert.Equal(t, string(tc.tier), entitlements.Tier) assert.Equal(t, tc.wantIsLifetime, entitlements.IsLifetime) - assert.Equal(t, int64(pkglicensing.TierMonitoredSystemLimits[tc.tier]), entitlementLimitByKey(entitlements.Limits, pkglicensing.MaxMonitoredSystemsLicenseGateKey)) + assertEntitlementLimitAbsent(t, entitlements.Limits, pkglicensing.MaxMonitoredSystemsLicenseGateKey) }) } } @@ -180,13 +179,18 @@ func responseRecorder() *httptest.ResponseRecorder { return httptest.NewRecorder() } -func entitlementLimitByKey(limits []pkglicensing.LimitStatus, key string) int64 { +func assertMonitoredSystemLimitAbsent(t *testing.T, limits map[string]int64) { + t.Helper() + assert.NotContains(t, limits, pkglicensing.MaxMonitoredSystemsLicenseGateKey) +} + +func assertEntitlementLimitAbsent(t *testing.T, limits []pkglicensing.LimitStatus, key string) { + t.Helper() for _, limit := range limits { if limit.Key == key { - return limit.Limit + t.Fatalf("entitlement limit %q present, want absent: %+v", key, limits) } } - return 0 } func managedLicenseServerDir(t *testing.T) string { @@ -229,21 +233,21 @@ func managedLicenseServerPlansJSON(t *testing.T) string { "tier": string(pkglicensing.TierLifetime), "duration_days": 0, "features": pkglicensing.TierFeatures[pkglicensing.TierLifetime], - "max_monitored_systems": pkglicensing.TierMonitoredSystemLimits[pkglicensing.TierLifetime], + "max_monitored_systems": 0, "max_guests": 5, }, "v5_pro_monthly_grandfathered": { "tier": string(pkglicensing.TierPro), "duration_days": 30, "features": pkglicensing.TierFeatures[pkglicensing.TierPro], - "max_monitored_systems": pkglicensing.TierMonitoredSystemLimits[pkglicensing.TierPro], + "max_monitored_systems": 0, "max_guests": 5, }, "v5_pro_annual_grandfathered": { "tier": string(pkglicensing.TierPro), "duration_days": 365, "features": pkglicensing.TierFeatures[pkglicensing.TierPro], - "max_monitored_systems": pkglicensing.TierMonitoredSystemLimits[pkglicensing.TierPro], + "max_monitored_systems": 0, "max_guests": 5, }, }