From 6998908d2a387f1ded356750c3ac9424f3beb04a Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:28:42 +0100 Subject: [PATCH] fix(release): limit asset validation readiness claims Downstream release-note syndication repeats the asset check banner even when installed health or release convergence is not qualified. Report asset checks only and state the remaining evidence boundaries for both draft and post-publication banners. Change-source: pulse-maintainer --- .github/workflows/validate-release-assets.yml | 7 ++- .../subsystems/deployment-installability.md | 7 +++ .../render_release_body_test.py | 50 +++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate-release-assets.yml b/.github/workflows/validate-release-assets.yml index d4930980c..c21f99cc8 100644 --- a/.github/workflows/validate-release-assets.yml +++ b/.github/workflows/validate-release-assets.yml @@ -410,8 +410,8 @@ jobs: INITIAL_STATE="${WORKFLOW_OUTPUT_1}" if [ "$INITIAL_STATE" = "true" ]; then HEADER_LINE="## ✅ Release Asset Validation: PASSED" - STATUS_LINE="**Status**: Ready for publication ✅" - INTRO_LINE="All release assets have been validated successfully!" + STATUS_LINE="**Status**: Release asset checks passed ✅" + INTRO_LINE="The required release asset checks passed." else HEADER_LINE="## ✅ Release Asset Validation (Post-Publish): PASSED" STATUS_LINE="**Status**: Live release assets re-validated ✅" @@ -450,6 +450,9 @@ jobs: - Version strings correct ✓ - Binary architectures validated ✓ + Asset validation alone does not establish installed-service health, + release convergence, clean soak or approval for stable publication. + EOF diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 3de6c7033..481dd5339 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -449,6 +449,13 @@ may delete invalid assets and rewrite validation annotations only while a release is still a draft. A post-publication edit is observation, not authority to mutate or destroy an immutable release; failed revalidation records a failing status and requires an explicit corrective release path. +Asset-validation banners report asset checks only, never publication readiness. +Both draft and post-publication banners must explicitly distinguish asset checks +from installed-service health, release convergence, clean soak and stable +publication approval, while retaining the individual check summary. +`scripts/release_control/render_release_body_test.py` executes the workflow +wording branch for both states and verifies these reporting limits; this local +proof does not qualify an installed service or authorise release mutation. The activation marker is part of that complete draft packet: its stored digest must be checked before publication, and customer convergence is forbidden until GitHub reports the release immutable and its signed release attestation verifies. diff --git a/scripts/release_control/render_release_body_test.py b/scripts/release_control/render_release_body_test.py index a939e5e24..7d4e8030c 100644 --- a/scripts/release_control/render_release_body_test.py +++ b/scripts/release_control/render_release_body_test.py @@ -4,6 +4,8 @@ from __future__ import annotations import re +import subprocess +import textwrap import tempfile import unittest from pathlib import Path @@ -814,5 +816,53 @@ Rollback details. self.assertIn("Push", blurb) +class AssetValidationWordingTest(unittest.TestCase): + def render_banner(self, draft: bool) -> str: + source = (_REPO_ROOT / ".github/workflows/validate-release-assets.yml").read_text(encoding="utf-8") + # Execute only the actual local wording branch and banner heredoc, + # never the workflow's GitHub reads/writes or release mutation code. + branch = source.split(' if [ "$INITIAL_STATE" = "true" ]; then\n', 1)[1] + branch = 'if [ "$INITIAL_STATE" = "true" ]; then\n' + branch.split( + "\n fi", 1 + )[0] + "\nfi\n" + template = textwrap.dedent(source.split( + " read -r -d '' VALIDATION_BLOCK <<'EOF' || true\n", 1 + )[1].split("\n EOF", 1)[0]) + result = subprocess.run( + ["bash", "-eu", "-c", 'INITIAL_STATE="$1"\n' + branch + + "printf '%s\\n' \"$HEADER_LINE\" \"$INTRO_LINE\" \"$STATUS_LINE\"", + "wording-test", str(draft).lower()], + check=True, capture_output=True, text=True, timeout=5, + ) + header, intro, status = result.stdout.splitlines() + return template.replace("HEADER_PLACEHOLDER", header).replace( + "INTRO_PLACEHOLDER", intro + ).replace("STATUS_PLACEHOLDER", status) + + def test_draft_reports_asset_checks_not_publication_readiness(self) -> None: + banner = self.render_banner(True) + self.assertIn("**Status**: Release asset checks passed", banner) + self.assertNotIn("Ready for publication", banner) + self.assertIn("## ✅ Release Asset Validation: PASSED", banner) + + def test_published_release_still_reports_revalidation(self) -> None: + banner = self.render_banner(False) + self.assertIn("Release Asset Validation (Post-Publish): PASSED", banner) + self.assertIn("Live release assets re-validated", banner) + self.assertNotIn("Ready for publication", banner) + + def test_both_banners_explain_limits_without_losing_check_summary(self) -> None: + for draft in (True, False): + with self.subTest(draft=draft): + banner = self.render_banner(draft) + self.assertIn("Asset validation alone does not establish installed-service health,", banner) + self.assertIn("release convergence, clean soak or approval for stable publication.", banner) + for check in ("All required assets present", "Checksums verified", + "Version strings correct", "Binary architectures validated"): + self.assertIn(check, banner) + self.assertIn("", banner) + self.assertIn("", banner) + + if __name__ == "__main__": unittest.main()