fix(release): bind Helm application version to chart release

Reject mismatched image defaults before packaging; preserve equal and default versions. Reproduced four accepted mismatches before the fix. All 58 focused tests pass; no publication performed.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-05 21:13:23 +01:00
parent d7f34995de
commit 96bc6f084f
6 changed files with 95 additions and 2 deletions
@@ -191,6 +191,9 @@ jobs:
- name: Run Helm Pages publication retry tests
run: python3 scripts/release_control/helm_pages_retry_test.py
- name: Helm publication version binding
run: python3 scripts/release_control/helm_publish_version_test.py
- name: Run status audit unit tests
run: python3 scripts/release_control/status_audit_test.py
+9 -2
View File
@@ -22,7 +22,7 @@ on:
required: true
type: string
app_version:
description: "Application version to embed (defaults to chart version)."
description: "Application version (must equal chart version; defaults to it)."
required: false
type: string
default: ""
@@ -36,7 +36,7 @@ on:
description: "Chart version (required when running manually, use format 4.24.0)"
required: true
app_version:
description: "Application version to embed (defaults to chart version)"
description: "Application version (must equal chart version; defaults to it)"
required: false
permissions:
@@ -90,6 +90,13 @@ jobs:
APP_VERSION="$CHART_VERSION"
fi
# Pulse's server and agent image defaults use Chart.AppVersion.
# Source provenance alone cannot detect a mismatched image override.
if [ "$APP_VERSION" != "$CHART_VERSION" ]; then
echo "::error::Release chart app_version must equal chart_version."
exit 1
fi
IS_PRERELEASE="false"
if [[ "$APP_VERSION" =~ -rc\.[0-9]+$ ]] || [[ "$APP_VERSION" =~ -alpha\.[0-9]+$ ]] || [[ "$APP_VERSION" =~ -beta\.[0-9]+$ ]]; then
IS_PRERELEASE="true"
+1
View File
@@ -242,6 +242,7 @@ scripts/release_control/*
!scripts/release_control/governance_stage_guard.py
!scripts/release_control/governance_stage_guard_test.py
!scripts/release_control/helm_pages_retry_test.py
!scripts/release_control/helm_publish_version_test.py
!scripts/release_control/live_runtime_proof.py
!scripts/release_control/live_runtime_proof_test.py
!scripts/release_control/mobile_relay_auth_approvals_proof.py
@@ -5168,3 +5168,20 @@ passed. A read-only probe of job 101235205647 retained the expected failure text
without ESC bytes. Private containment classification and successful scheduled
reconciliation still require post-integration evidence; this is not customer
convergence or release qualification.
### 2026-09-05 — Bind hosted chart application version to release version
The hosted Helm publisher rejects a supplied application version that differs
from the chart version before emitting version outputs or packaging. Both server
and agent image defaults use Chart.AppVersion, so an exact source SHA and chart
digest alone do not prevent an override from selecting another release's images.
The normal release caller already supplies equal versions; default and release
event paths remain unchanged. This does not remove users' image value overrides.
Verification: helm_publish_version_test.py executes the actual version-resolution
shell. Four mismatches failed assertions before the fix and are rejected after it;
stable/alpha/beta/RC equal and default versions and release-event defaults pass.
All 4 tests, 7 Helm Pages retry tests and 47 promotion-policy tests pass locally.
No hosted publication or installed-image qualification is claimed.
External reference retrieved 2026-09-05: https://helm.sh/docs/topics/charts/#the-appversion-field
explains that application version is separate from chart version.
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Exercise hosted Helm version resolution without packaging or publication."""
from pathlib import Path
import os
import subprocess
import tempfile
import textwrap
import unittest
ROOT = Path(__file__).resolve().parents[2]
class HelmPublishVersionTests(unittest.TestCase):
def resolve(self, chart="", app="", tag=""):
workflow = (ROOT / ".github/workflows/publish-helm-chart.yml").read_text()
step = workflow.split(" - name: Determine chart version\n", 1)[1]
script = textwrap.dedent(step.split(" run: |\n", 1)[1].split(
" - name:", 1)[0])
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp) / "output"
result = subprocess.run(
["bash", "-euo", "pipefail", "-c", script], cwd=ROOT,
env={"PATH": os.environ["PATH"], "INPUT_CHART_VERSION": chart,
"INPUT_APP_VERSION": app, "RELEASE_TAG_NAME": tag,
"GITHUB_OUTPUT": str(output)},
capture_output=True, text=True)
return result, output.read_text() if output.exists() else ""
def test_matching_and_default_application_versions(self):
for version in ("6.4.3", "6.4.3-rc.2", "6.5.0-beta.1", "6.5.0-alpha.1"):
for app in ("", version):
with self.subTest(version=version, app=app):
result, output = self.resolve(version, app)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("\n" + version + "\n", output)
self.assertIn("is_prerelease=" + ("true" if "-" in version else "false"), output)
def test_release_event_uses_tag(self):
result, output = self.resolve(tag="v6.4.3-rc.2")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("\n6.4.3-rc.2\n", output)
def test_mismatched_application_rejected_before_outputs(self):
for chart, app in (("6.4.3", "6.4.2"), ("6.4.3", "6.4.3-rc.2"),
("6.4.3-rc.2", "6.4.3"), ("6.4.3", "latest")):
with self.subTest(chart=chart, app=app):
result, output = self.resolve(chart, app)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(output, "")
def test_missing_version_rejected(self):
result, output = self.resolve()
self.assertNotEqual(result.returncode, 0)
self.assertEqual(output, "")
if __name__ == "__main__":
unittest.main()
@@ -2434,6 +2434,13 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("sync_chart_release_metadata.py", helm)
self.assertNotIn("sync_chart_release_metadata.py", helm_pages)
self.assertIn("--chart deploy/helm/pulse/Chart.yaml", helm)
version_guard = 'if [ "$APP_VERSION" != "$CHART_VERSION" ]; then'
self.assertIn(version_guard, helm)
self.assertLess(helm.index(version_guard), helm.index(" - name: Package chart"))
self.assertIn(
"python3 scripts/release_control/helm_publish_version_test.py",
(Path(__file__).resolve().parents[2] / ".github/workflows/canonical-governance.yml").read_text(),
)
self.assertIn('git checkout --detach "refs/tags/${RELEASE_TAG}"', helm)
self.assertIn("Verify public GHCR chart identity and provenance", helm)
self.assertIn("helm registry logout ghcr.io || true", helm)