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()