From c384d5e0c828bbd9a835afa15780f6310bc6204f Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 5 Jul 2026 11:07:27 +0100 Subject: [PATCH] Prepare v6.0.3 patch release --- VERSION | 2 +- deploy/helm/pulse/Chart.yaml | 8 +-- deploy/helm/pulse/README.md | 2 +- docker-compose.yml | 2 +- docs/RELEASE_NOTES.md | 2 + docs/UPGRADE_v6.md | 4 +- .../subsystems/deployment-installability.md | 10 +-- docs/releases/RELEASE_NOTES_v6.0.3.md | 32 +++++++++ docs/releases/V6_CHANGELOG_v6.0.3.md | 21 ++++++ .../src/api/__tests__/connections.test.ts | 4 +- .../InfrastructureWorkspace.test.tsx | 8 +-- internal/api/connections_aggregator_test.go | 16 ++--- internal/api/contract_test.go | 4 +- scripts/install-docker.sh | 2 +- .../installtests/build_release_assets_test.go | 72 +++++++++++++++++++ .../installtests/install_docker_sh_test.go | 17 +++++ 16 files changed, 175 insertions(+), 31 deletions(-) create mode 100644 docs/releases/RELEASE_NOTES_v6.0.3.md create mode 100644 docs/releases/V6_CHANGELOG_v6.0.3.md diff --git a/VERSION b/VERSION index 9b9a24420..090ea9dad 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.2 +6.0.3 diff --git a/deploy/helm/pulse/Chart.yaml b/deploy/helm/pulse/Chart.yaml index 046c567e6..7a7f0d2d5 100644 --- a/deploy/helm/pulse/Chart.yaml +++ b/deploy/helm/pulse/Chart.yaml @@ -2,9 +2,9 @@ apiVersion: v2 name: pulse description: Helm chart for deploying the Pulse hub and optional Docker monitoring agent. type: application -version: 6.0.2 -appVersion: "6.0.2" -icon: https://raw.githubusercontent.com/rcourtman/Pulse/v6.0.2/docs/images/pulse-logo.svg +version: 6.0.3 +appVersion: "6.0.3" +icon: https://raw.githubusercontent.com/rcourtman/Pulse/v6.0.3/docs/images/pulse-logo.svg keywords: - monitoring - proxmox @@ -32,7 +32,7 @@ annotations: description: Smoke tests with kind cluster deployment artifacthub.io/links: | - name: Documentation - url: https://github.com/rcourtman/Pulse/blob/v6.0.2/docs/KUBERNETES.md + url: https://github.com/rcourtman/Pulse/blob/v6.0.3/docs/KUBERNETES.md - name: Support url: https://github.com/rcourtman/Pulse/discussions artifacthub.io/maintainers: | diff --git a/deploy/helm/pulse/README.md b/deploy/helm/pulse/README.md index 34692b14b..5b786836b 100644 --- a/deploy/helm/pulse/README.md +++ b/deploy/helm/pulse/README.md @@ -1,6 +1,6 @@ # pulse -![Version: 6.0.2](https://img.shields.io/badge/Version-6.0.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 6.0.2](https://img.shields.io/badge/AppVersion-6.0.2-informational?style=flat-square) +![Version: 6.0.3](https://img.shields.io/badge/Version-6.0.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 6.0.3](https://img.shields.io/badge/AppVersion-6.0.3-informational?style=flat-square) Helm chart for deploying the Pulse hub and optional Docker monitoring agent. diff --git a/docker-compose.yml b/docker-compose.yml index b17ffd3ef..75ccb59c8 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.2} + image: ${PULSE_IMAGE:-rcourtman/pulse:6.0.3} container_name: pulse restart: unless-stopped logging: diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index ffc8f3a3b..ed8bcfbbc 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -7,6 +7,8 @@ For historical v4 notes that previously lived in this repo, see: `docs/releases/RELEASE_NOTES_v4.md` For the current stable v6 patch packet, see: +- `docs/releases/RELEASE_NOTES_v6.0.3.md` +- `docs/releases/V6_CHANGELOG_v6.0.3.md` - `docs/releases/RELEASE_NOTES_v6.0.2.md` - `docs/releases/V6_CHANGELOG_v6.0.2.md` - `docs/UPGRADE_v6.md` diff --git a/docs/UPGRADE_v6.md b/docs/UPGRADE_v6.md index 8ab11dea7..6afde744d 100644 --- a/docs/UPGRADE_v6.md +++ b/docs/UPGRADE_v6.md @@ -4,8 +4,8 @@ This guide covers practical upgrade steps for existing Pulse installs moving to For the current stable v6 packet and rollout references, see: -- `docs/releases/RELEASE_NOTES_v6.0.2.md` -- `docs/releases/V6_CHANGELOG_v6.0.2.md` +- `docs/releases/RELEASE_NOTES_v6.0.3.md` +- `docs/releases/V6_CHANGELOG_v6.0.3.md` - `docs/releases/RELEASE_NOTES_v6.md` - `docs/releases/V6_CHANGELOG.md` diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index bdcc789a8..99a95e2cd 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -793,10 +793,10 @@ TLS floor in the dynamic config. ## Current State -The active stable `v6.0.2` cut sets the repo-root `VERSION`, repo-root +The active stable `v6.0.3` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and -Helm chart release metadata to the same `6.0.2` release version. This patch -release uses the stable hotfix path with `rollback_version=v6.0.1`, +Helm chart release metadata to the same `6.0.3` release version. This patch +release uses the stable hotfix path with `rollback_version=v6.0.2`, `hotfix_exception=true`, a release-owner reason, and no fabricated same-version RC tag. The initial GA promotion metadata remains `promoted_from_tag=v6.0.0-rc.7`, `rollback_version=v5.1.35`, @@ -850,8 +850,8 @@ compose image default, standalone installer fallback constant, and packaged Helm metadata. A draft release workflow failure caused by stale image or chart pins is a release-packet blocker until the defaults, tests, and evidence record are refreshed from the new branch head. -For the active stable `v6.0.2` cut, the repo-root compose default and -`scripts/install-docker.sh` fallback must both pin `6.0.2`; the stable +For the active stable `v6.0.3` cut, the repo-root compose default and +`scripts/install-docker.sh` fallback must both pin `6.0.3`; the stable promotion guard remains in force by rejecting leftover `-rc.` defaults when the governed `VERSION` is a stable release. The RC7 packet refresh records `fc10de9b5477613316473267b72b05b6b2b7aaff` diff --git a/docs/releases/RELEASE_NOTES_v6.0.3.md b/docs/releases/RELEASE_NOTES_v6.0.3.md new file mode 100644 index 000000000..f4645640b --- /dev/null +++ b/docs/releases/RELEASE_NOTES_v6.0.3.md @@ -0,0 +1,32 @@ +# Pulse v6.0.3 Release Notes + +`v6.0.3` is a stable patch release for the Pulse v6 line. It follows +`v6.0.2` and focuses on the remaining v5-to-v6 agent upgrade failure reported +after the first two v6 patch releases. + +## Fixes + +- Fixed v5 agent update recovery when the generated update command supplies an + explicit Pulse URL but the old running v5 agent process is still the only + source for the token and enabled telemetry scopes. The installer now merges + the explicit URL with the recovered process state instead of failing with + "No existing Pulse Agent connection state found." +- Treated incomplete saved v6 agent state as recoverable during `--update`. + This keeps the update path on the legacy recovery branch whenever any + required connection detail is still missing. +- Allowed carrier-grade NAT / overlay network addresses in `100.64.0.0/10` as + local-network HTTP and WebSocket targets for Pulse agent connections. This + fixes deployments that legitimately use addresses such as + `http://100.100.100.5:7655` while keeping public non-local addresses on the + HTTPS-required path. +- Refreshed Docker, Helm, and installer release metadata for the stable patch + line. + +## Upgrade Notes + +Use the normal v6 install or update flow for `v6.0.3`. The rollback target for +this patch release is `v6.0.2`. + +If a v5.1.35 agent upgrade failed on `v6.0.0`, `v6.0.1`, or `v6.0.2`, rerun the +current update command from **Settings -> Infrastructure** after updating the +Pulse server to `v6.0.3`. diff --git a/docs/releases/V6_CHANGELOG_v6.0.3.md b/docs/releases/V6_CHANGELOG_v6.0.3.md new file mode 100644 index 000000000..53068be16 --- /dev/null +++ b/docs/releases/V6_CHANGELOG_v6.0.3.md @@ -0,0 +1,21 @@ +# Pulse v6.0.3 + +_This changelog describes the stable `v6.0.3` patch release compared with +`v6.0.2`._ + +## Fixed + +- Legacy v5 agent updates now recover complete connection state when saved v6 + state is missing or incomplete. +- Agent update commands now merge explicit Pulse URLs with token and scope + details recovered from the running v5 agent process. +- Agent URL validation now treats `100.64.0.0/10` carrier-grade NAT and overlay + addresses as local network targets for HTTP and WebSocket URLs. +- Docker, Helm, and installer release metadata now track the active stable + patch version. + +## Release Metadata + +- Version: `v6.0.3` +- Rollback target: `v6.0.2` +- Promotion path: stable patch hotfix from `pulse/v6-release` diff --git a/frontend-modern/src/api/__tests__/connections.test.ts b/frontend-modern/src/api/__tests__/connections.test.ts index 9c2903c76..247d5aceb 100644 --- a/frontend-modern/src/api/__tests__/connections.test.ts +++ b/frontend-modern/src/api/__tests__/connections.test.ts @@ -88,7 +88,7 @@ describe('ConnectionsAPI', () => { lastError: null, source: 'agent', agentVersion: '6.0.0', - expectedAgentVersion: '6.0.2', + expectedAgentVersion: '6.0.3', agentUpdateAvailable: true, capabilities: { supportsPause: false, supportsScope: false, supportsTest: false }, }, @@ -99,7 +99,7 @@ describe('ConnectionsAPI', () => { expect(result.connections[0]).toMatchObject({ agentVersion: '6.0.0', - expectedAgentVersion: '6.0.2', + expectedAgentVersion: '6.0.3', agentUpdateAvailable: true, }); }); diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx index 673f599a2..dd5d9230e 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx @@ -906,7 +906,7 @@ describe('InfrastructureWorkspace', () => { surfaces: ['host'], scope: { host: true } as any, source: 'agent', - agentVersion: '6.0.2', + agentVersion: '6.0.3', agentIdentity: { hostname: 'tower', platform: 'linux', @@ -970,7 +970,7 @@ describe('InfrastructureWorkspace', () => { await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); expect(screen.getByText('Pulse Agent version')).toBeInTheDocument(); - expect(screen.getByText('6.0.2')).toBeInTheDocument(); + expect(screen.getByText('6.0.3')).toBeInTheDocument(); expect(screen.getByText('Operating system')).toBeInTheDocument(); expect(screen.getAllByText('Unraid 7.1.0').length).toBeGreaterThan(0); expect(screen.getByText('Reported hostname')).toBeInTheDocument(); @@ -996,7 +996,7 @@ describe('InfrastructureWorkspace', () => { scope: { host: true } as any, lastSeen: new Date().toISOString(), agentVersion: '6.0.0', - expectedAgentVersion: '6.0.2', + expectedAgentVersion: '6.0.3', agentUpdateAvailable: true, capabilities: { supportsPause: true, supportsScope: true, supportsTest: true }, }); @@ -1042,7 +1042,7 @@ describe('InfrastructureWorkspace', () => { expect(screen.getByText('Pulse Agent augmentation')).toBeInTheDocument(); expect(screen.getByText('zeus-agent')).toBeInTheDocument(); expect(screen.getByText('Update available')).toBeInTheDocument(); - expect(screen.getByText('6.0.0 -> 6.0.2')).toBeInTheDocument(); + expect(screen.getByText('6.0.0 -> 6.0.3')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Copy uninstall command/i })).toBeInTheDocument(); }); diff --git a/internal/api/connections_aggregator_test.go b/internal/api/connections_aggregator_test.go index 818c2acd3..0effbf722 100644 --- a/internal/api/connections_aggregator_test.go +++ b/internal/api/connections_aggregator_test.go @@ -330,11 +330,11 @@ func TestBuildConnections_AgentVersionUpdateAvailability(t *testing.T) { now := time.Now() in := aggregatorInputs{ hosts: []models.Host{ - {ID: "current", Hostname: "current", LastSeen: now, AgentVersion: "6.0.2"}, + {ID: "current", Hostname: "current", LastSeen: now, AgentVersion: "6.0.3"}, {ID: "outdated", Hostname: "outdated", LastSeen: now, AgentVersion: "6.0.0"}, {ID: "unknown", Hostname: "unknown", LastSeen: now}, }, - expectedAgentVersion: "6.0.2", + expectedAgentVersion: "6.0.3", now: now, } @@ -344,14 +344,14 @@ func TestBuildConnections_AgentVersionUpdateAvailability(t *testing.T) { byID[connection.ID] = connection } - if byID["agent:current"].AgentVersion != "6.0.2" { - t.Fatalf("current agent version = %q, want %q", byID["agent:current"].AgentVersion, "6.0.2") + if byID["agent:current"].AgentVersion != "6.0.3" { + t.Fatalf("current agent version = %q, want %q", byID["agent:current"].AgentVersion, "6.0.3") } - if byID["agent:current"].ExpectedAgentVersion != "6.0.2" { + if byID["agent:current"].ExpectedAgentVersion != "6.0.3" { t.Fatalf( "current expected agent version = %q, want %q", byID["agent:current"].ExpectedAgentVersion, - "6.0.2", + "6.0.3", ) } if byID["agent:current"].AgentUpdateAvailable { @@ -387,7 +387,7 @@ func TestBuildConnections_AgentFleetGovernance(t *testing.T) { ID: "current", Hostname: "current", LastSeen: now, - AgentVersion: "6.0.2", + AgentVersion: "6.0.3", CommandsEnabled: true, }, { @@ -404,7 +404,7 @@ func TestBuildConnections_AgentFleetGovernance(t *testing.T) { agentDesiredConfigs: map[string]connectionAgentDesiredConfig{ "current": currentDesiredConfig, }, - expectedAgentVersion: "6.0.2", + expectedAgentVersion: "6.0.3", now: now, } diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go index 712a0f4ca..5eff70191 100644 --- a/internal/api/contract_test.go +++ b/internal/api/contract_test.go @@ -14549,7 +14549,7 @@ func TestContract_AgentConnectionPayloadIncludesVersionFields(t *testing.T) { CommandsEnabled: true, }, AgentVersion: "6.0.0", - ExpectedAgentVersion: "6.0.2", + ExpectedAgentVersion: "6.0.3", AgentUpdateAvailable: true, Fleet: ConnectionFleetGovernance{ EnrollmentState: fleetStateEnrolled, @@ -14583,7 +14583,7 @@ func TestContract_AgentConnectionPayloadIncludesVersionFields(t *testing.T) { t.Fatalf("agent platform must remain runtime platform, got profile id %q", conn.AgentIdentity.Platform) } - want := `{"id":"agent:host-1","type":"agent","name":"host-1","address":"host-1","hostAliases":["host-1","192.168.0.2"],"state":"active","enabled":true,"surfaces":["host"],"scope":{"host":true},"lastSeen":"2026-04-22T12:00:00Z","source":"agent","agentIdentity":{"hostname":"host-1","platform":"linux","hostProfile":"unraid","osName":"Unraid","osVersion":"7.1.0","kernelVersion":"6.12.0","architecture":"x86_64","reportIp":"192.168.0.2","commandsEnabled":true},"agentVersion":"6.0.0","expectedAgentVersion":"6.0.2","agentUpdateAvailable":true,"fleet":{"enrollmentState":"enrolled","livenessState":"active","versionDrift":"behind","adapterHealth":"healthy","configRollout":"reported","credentialStatus":"verified","updateStatus":"update-available","remoteControl":"enabled","commandPolicy":{"status":"enabled","desired":"enabled","applied":"enabled","enforcement":"in-sync","reason":"agent command execution matches the desired enabled policy"}},"capabilities":{"supportsPause":false,"supportsScope":false,"supportsTest":false}}` + want := `{"id":"agent:host-1","type":"agent","name":"host-1","address":"host-1","hostAliases":["host-1","192.168.0.2"],"state":"active","enabled":true,"surfaces":["host"],"scope":{"host":true},"lastSeen":"2026-04-22T12:00:00Z","source":"agent","agentIdentity":{"hostname":"host-1","platform":"linux","hostProfile":"unraid","osName":"Unraid","osVersion":"7.1.0","kernelVersion":"6.12.0","architecture":"x86_64","reportIp":"192.168.0.2","commandsEnabled":true},"agentVersion":"6.0.0","expectedAgentVersion":"6.0.3","agentUpdateAvailable":true,"fleet":{"enrollmentState":"enrolled","livenessState":"active","versionDrift":"behind","adapterHealth":"healthy","configRollout":"reported","credentialStatus":"verified","updateStatus":"update-available","remoteControl":"enabled","commandPolicy":{"status":"enabled","desired":"enabled","applied":"enabled","enforcement":"in-sync","reason":"agent command execution matches the desired enabled policy"}},"capabilities":{"supportsPause":false,"supportsScope":false,"supportsTest":false}}` assertJSONSnapshot(t, body, want) } diff --git a/scripts/install-docker.sh b/scripts/install-docker.sh index 7ca066396..3b3183764 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.2" +CANONICAL_DEFAULT_PULSE_VERSION="6.0.3" resolve_default_pulse_version() { if [ -n "${PULSE_IMAGE_VERSION:-}" ]; then diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 89493fbae..0ca702ffd 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -240,6 +240,60 @@ func TestCreateReleaseUploadsPowerShellInstaller(t *testing.T) { } } +func TestCurrentStablePatchReleasePacketTracksInstallMetadata(t *testing.T) { + version := currentReleaseVersion(t) + if isPrereleaseVersion(version) { + t.Skip("current release is a prerelease") + } + previous, ok := previousStablePatchVersion(version) + if !ok { + t.Skip("current release is not a stable patch release") + } + + releaseNotesPath := repoFile("docs", "releases", "RELEASE_NOTES_v"+version+".md") + changelogPath := repoFile("docs", "releases", "V6_CHANGELOG_v"+version+".md") + + assertFileContainsAllNormalized(t, releaseNotesPath, + "`v"+version+"` is a stable patch release", + "`v"+previous+"`", + "rollback target for this patch release is `v"+previous+"`", + ) + assertFileContainsAll(t, changelogPath, + "Version: `v"+version+"`", + "Rollback target: `v"+previous+"`", + "Promotion path: stable patch hotfix from `pulse/v6-release`", + ) + assertFileContainsAll(t, repoFile("docs", "RELEASE_NOTES.md"), + "docs/releases/RELEASE_NOTES_v"+version+".md", + "docs/releases/V6_CHANGELOG_v"+version+".md", + ) + assertFileContainsAll(t, repoFile("docs", "UPGRADE_v6.md"), + "docs/releases/RELEASE_NOTES_v"+version+".md", + "docs/releases/V6_CHANGELOG_v"+version+".md", + ) + assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "Chart.yaml"), + "version: "+version, + `appVersion: "`+version+`"`, + "raw.githubusercontent.com/rcourtman/Pulse/v"+version+"/docs/images/pulse-logo.svg", + "blob/v"+version+"/docs/KUBERNETES.md", + ) + assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "README.md"), + "Version-"+version+"-informational", + "AppVersion-"+version+"-informational", + ) + assertFileContainsAll(t, repoFile("docker-compose.yml"), + "image: ${PULSE_IMAGE:-rcourtman/pulse:"+version+"}", + ) + assertFileContainsAll(t, repoFile("scripts", "install-docker.sh"), + `CANONICAL_DEFAULT_PULSE_VERSION="`+version+`"`, + ) + assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"), + "The active stable `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.", + "This patch release uses the stable hotfix path with `rollback_version=v"+previous+"`, `hotfix_exception=true`, a release-owner reason, and no fabricated same-version RC tag.", + "For the active stable `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"`", + ) +} + func TestBackfillReleaseWorkflowRepairsPublishedAssetsWithoutRebuilds(t *testing.T) { scriptBytes, err := os.ReadFile(repoFile("scripts", "backfill-release-assets.sh")) if err != nil { @@ -1297,6 +1351,24 @@ func assertFileContainsAll(t *testing.T, path string, required ...string) { } } +func assertFileContainsAllNormalized(t *testing.T, path string, required ...string) { + t.Helper() + content, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + s := normalizedInstallTestWhitespace(string(content)) + for _, needle := range required { + if !strings.Contains(s, normalizedInstallTestWhitespace(needle)) { + t.Fatalf("%s missing required normalized substring: %s", path, needle) + } + } +} + +func normalizedInstallTestWhitespace(text string) string { + return strings.Join(strings.Fields(text), " ") +} + func workflowJobBlock(t *testing.T, workflow, job string) string { t.Helper() diff --git a/scripts/installtests/install_docker_sh_test.go b/scripts/installtests/install_docker_sh_test.go index 37ab3bc5e..da92773ea 100644 --- a/scripts/installtests/install_docker_sh_test.go +++ b/scripts/installtests/install_docker_sh_test.go @@ -155,6 +155,23 @@ func TestInstallDockerScriptFallbackPinsCurrentVersion(t *testing.T) { } } +func TestInstallDockerProofTracksStablePatchReleaseContract(t *testing.T) { + version := currentReleaseVersion(t) + if isPrereleaseVersion(version) { + t.Skip("current release is a prerelease") + } + previous, ok := previousStablePatchVersion(version) + if !ok { + t.Skip("current release is not a stable patch release") + } + + assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"), + "The active stable `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.", + "This patch release uses the stable hotfix path with `rollback_version=v"+previous+"`, `hotfix_exception=true`, a release-owner reason, and no fabricated same-version RC tag.", + "For the active stable `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"`", + ) +} + func runInstallDockerScript(t *testing.T, workDir string, envVars ...string) { t.Helper()