diff --git a/.github/workflows/helm-pages.yml b/.github/workflows/helm-pages.yml index 970dfd3ce..3ca3e13d3 100644 --- a/.github/workflows/helm-pages.yml +++ b/.github/workflows/helm-pages.yml @@ -275,6 +275,10 @@ jobs: set -euo pipefail public_repo="https://rcourtman.github.io/Pulse" public_ready=false + public_work="$(mktemp -d)" + trap 'rm -rf "${public_work}"' EXIT + qualified_chart="dist/pulse-${VERSION}.tgz" + test -f "${qualified_chart}" for attempt in $(seq 1 30); do public_index="$(mktemp)" if curl -fsSL --retry 2 --retry-delay 1 --retry-all-errors \ @@ -283,7 +287,17 @@ jobs: helm repo remove pulse-public >/dev/null 2>&1 || true helm repo add pulse-public "${public_repo}" --force-update helm repo update pulse-public - if helm show chart pulse-public/pulse --version "${VERSION}" >/dev/null; then + # Metadata readability is not an exact-artifact receipt. Pull via + # the consumer index and compare with the OCI-qualified package, + # not the OCI manifest digest (which hashes a different object). + rm -f "${public_work}/pulse-${VERSION}.tgz" + if helm pull pulse-public/pulse --version "${VERSION}" --destination "${public_work}" && \ + helm show chart "${public_work}/pulse-${VERSION}.tgz" >/dev/null; then + if ! cmp -s "${qualified_chart}" "${public_work}/pulse-${VERSION}.tgz"; then + echo "::error::Public Helm chart bytes differ from the qualified package; refusing a successful convergence receipt." + rm -f "${public_index}" + exit 1 + fi public_ready=true rm -f "${public_index}" break @@ -297,4 +311,4 @@ jobs: echo "::error::Public Helm repository did not expose chart ${VERSION}." exit 1 fi - echo "[OK] Public Helm repository serves pulse ${VERSION}." + echo "[OK] Public Helm repository serves the exact qualified pulse ${VERSION} package." diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index a5c18c6ec..94e05405a 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -15,6 +15,22 @@ ## Purpose +### Public Helm exact-package receipt + +The post-activation public Pages verification in `.github/workflows/helm-pages.yml` +must pull the requested chart version through the consumer repository and compare +its archive bytes with the package already recovered through OCI qualification. +Readable chart metadata alone is not a successful convergence receipt. The OCI +manifest digest hashes a different object and must not be compared directly with +the archive. A byte mismatch fails immediately; unavailable downloads retain the +bounded retry, with previous downloaded files removed before each attempt. +Activation bindings, publication authority and containment remain prerequisites. +The executed-shell fixtures in `scripts/release_control/helm_pages_retry_test.py` +cover matching, mismatched, missing and unavailable public packages without +network or publication writes. They are not installed Helm qualification or proof +that any historical published chart was wrong or has been repaired. + + The shell-owned multi-tenant integration suite uses a dedicated desktop-only Playwright configuration selecting the seven multi-tenant scenarios. It must reject any non-empty E2E tier identity rather than impersonating stable or diff --git a/scripts/release_control/helm_pages_retry_test.py b/scripts/release_control/helm_pages_retry_test.py index 4ccfb78f0..2625cf3fa 100644 --- a/scripts/release_control/helm_pages_retry_test.py +++ b/scripts/release_control/helm_pages_retry_test.py @@ -106,5 +106,56 @@ class HelmPagesRetryTests(unittest.TestCase): self.assertIn("--latest=false", calls[-1]) +class PublicReceiptTests(unittest.TestCase): + def receipt(self, mode): + workflow = (ROOT / '.github/workflows/helm-pages.yml').read_text() + step = workflow.split(' - name: Verify public Pages chart\n', 1)[1] + script = textwrap.dedent(step.split(' run: |\n', 1)[1]) + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + (root / 'dist').mkdir() + (root / 'dist/pulse-6.4.3.tgz').write_bytes(b'qualified') + programs = { + 'curl': '''#!/usr/bin/env python3 +import pathlib, sys +pathlib.Path(sys.argv[sys.argv.index('-o')+1]).write_text('version: 6.4.3\\n') +''', + 'helm': '''#!/usr/bin/env python3 +import os, pathlib, sys +args = sys.argv[1:] +if args[0] == 'pull': + mode = os.environ['MODE'] + if mode == 'unavailable': sys.exit(1) + if mode != 'missing': + dest = pathlib.Path(args[args.index('--destination')+1]) + (dest / 'pulse-6.4.3.tgz').write_bytes(b'qualified' if mode == 'match' else b'wrong') +''', + 'sleep': '#!/bin/sh\nexit 0\n', + } + for name, body in programs.items(): + path = root / name + path.write_text(body) + path.chmod(0o755) + return subprocess.run(['bash', '-c', script], cwd=root, + env={'PATH': f"{root}:{os.environ['PATH']}", + 'VERSION': '6.4.3', 'MODE': mode}, + capture_output=True, text=True) + + def test_matching_public_package_passes(self): + result = self.receipt('match') + self.assertEqual(result.returncode, 0, result.stderr) + + def test_same_version_wrong_bytes_fail(self): + result = self.receipt('wrong') + self.assertNotEqual(result.returncode, 0) + self.assertNotIn('[OK]', result.stdout) + + def test_missing_download_fails(self): + self.assertNotEqual(self.receipt('missing').returncode, 0) + + def test_unavailable_download_fails(self): + self.assertNotEqual(self.receipt('unavailable').returncode, 0) + + if __name__ == "__main__": unittest.main() diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 35ce7ca0f..f7ba48efc 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -2424,7 +2424,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn('helm repo index "${index_work}"', helm_pages) self.assertIn('git -C gh-pages push origin HEAD:gh-pages', helm_pages) self.assertIn('grep -q "version: ${VERSION}"', helm_pages) - self.assertIn('helm show chart pulse-public/pulse --version "${VERSION}"', helm_pages) + self.assertIn('helm pull pulse-public/pulse --version "${VERSION}"', helm_pages) + self.assertIn('cmp -s "${qualified_chart}" "${public_work}/pulse-${VERSION}.tgz"', helm_pages) self.assertNotIn("helm status pulse || true", helm_pages) self.assertNotIn("kubectl describe pods", helm_pages) self.assertIn("release-convergence.yml/dispatches", release_workflow)