fix(release): reject draft Helm chart publication retries

Reproduce draft and unknown publication states reaching the Pages index boundary. Require an explicitly non-draft existing release before uploading, editing or advertising its chart, without implicitly publishing operator drafts.

Exercise the actual publication shell with a fake GitHub CLI and wire its seven retry tests into canonical governance. Existing digest and maturity behaviour remains covered.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-05 20:53:21 +01:00
parent daaa201fd7
commit 7c373a5162
6 changed files with 149 additions and 2 deletions
@@ -188,6 +188,9 @@ jobs:
- name: Run stable release continuity diagnostic unit tests
run: python3 scripts/release_control/release_continuity_test.py
- name: Run Helm Pages publication retry tests
run: python3 scripts/release_control/helm_pages_retry_test.py
- name: Run status audit unit tests
run: python3 scripts/release_control/status_audit_test.py
+11 -2
View File
@@ -200,8 +200,17 @@ jobs:
fi
local_digest="sha256:$(sha256sum "${chart_path}" | cut -d ' ' -f 1)"
if existing_prerelease="$(gh release view "${chart_release}" --repo "${GITHUB_REPOSITORY}" \
--json isPrerelease --jq '.isPrerelease' 2>/dev/null)"; then
if existing_state="$(gh release view "${chart_release}" --repo "${GITHUB_REPOSITORY}" \
--json isPrerelease,isDraft --jq '[.isPrerelease, .isDraft] | @tsv' 2>/dev/null)"; then
existing_prerelease="$(awk -F '\t' '{print $1}' <<<"${existing_state}")"
existing_draft="$(awk -F '\t' '{print $2}' <<<"${existing_state}")"
# A draft asset is visible to this authenticated workflow but not
# to Helm consumers. Do not turn an interrupted publication into
# a public index entry, or implicitly publish an operator's draft.
if [[ "${existing_draft}" != "false" ]]; then
echo "::error::Chart release ${chart_release} is not confirmed published; refusing to modify it or advertise it through Pages."
exit 1
fi
# Immutable releases refuse asset replacement, so a convergence
# retry must recognise the exact chart it already published
# rather than clobber it (v6.4.3-rc.1 retry, 2026-09-02). The
+1
View File
@@ -241,6 +241,7 @@ scripts/release_control/*
!scripts/release_control/format_staged_go_test.py
!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/live_runtime_proof.py
!scripts/release_control/live_runtime_proof_test.py
!scripts/release_control/mobile_relay_auth_approvals_proof.py
@@ -1140,6 +1140,15 @@ artifact-selection behaviour.
and qualified by the exact create-release run. It must bind that artifact
to the activated source run, tag, commit, and activation marker, and must
not repeat chart packaging or the pre-activation kind install/upgrade smoke.
An existing chart release must explicitly report a non-draft state before
convergence uploads assets, edits metadata, or advertises it through Pages.
Matching assets visible to the authenticated workflow do not make a draft
publicly downloadable. Draft or unknown publication state fails closed
without implicitly publishing an operator-owned draft. Published matching
assets remain reusable without replacement. The executable retry fixtures
in `scripts/release_control/helm_pages_retry_test.py` and the release-policy
test `test_helm_pages_retry_requires_explicitly_published_chart` protect this
boundary; they are not live publication or installed Helm qualification.
Release-to-convergence and cross-repository child-run observation should
use short bounded polls so GitHub indexing cannot add tens of seconds after
a required exact run or activation marker has already completed.
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Execute the chart publication shell with a fake GitHub CLI; no network/writes."""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
import subprocess
import tempfile
import textwrap
import unittest
ROOT = Path(__file__).resolve().parents[2]
CHART = b"qualified chart fixture"
DIGEST = "sha256:" + hashlib.sha256(CHART).hexdigest()
class HelmPagesRetryTests(unittest.TestCase):
def run_publication(self, *, draft=False, digest=DIGEST, prerelease=False,
version="6.4.3", exists=True):
workflow = (ROOT / ".github/workflows/helm-pages.yml").read_text()
step = workflow.split(" - name: Publish chart release and merge Pages index\n", 1)[1]
script = textwrap.dedent(step.split(" run: |\n", 1)[1].split(
" git -C gh-pages config", 1)[0])
script = script.replace("${{ github.repository }}", "test/pulse")
script += '\nprintf "PAGES_BOUNDARY\\n"\n'
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
(root / "dist").mkdir()
(root / "dist" / f"pulse-{version}.tgz").write_bytes(CHART)
fake = root / "gh"
fake.write_text(textwrap.dedent('''\
#!/usr/bin/env python3
import json, os, subprocess, sys
args = sys.argv[1:]
with open("calls.jsonl", "a") as log:
log.write(json.dumps(args) + "\\n")
if args[:2] == ["release", "view"]:
if os.environ["EXISTS"] == "false":
sys.exit(1)
payload = {"isDraft": json.loads(os.environ["DRAFT"]),
"isPrerelease": json.loads(os.environ["PRERELEASE"])}
elif args[0] == "api":
payload = {"assets": [{"name": "pulse-" + os.environ["VERSION"] + ".tgz",
"digest": os.environ["DIGEST"]}]}
else:
sys.exit(0)
result = subprocess.run(["jq", "-r", args[args.index("--jq") + 1]],
input=json.dumps(payload), text=True)
sys.exit(result.returncode)
'''))
fake.chmod(0o755)
env = {"PATH": f"{root}:{os.environ['PATH']}", "VERSION": version,
"GITHUB_REPOSITORY": "test/pulse", "TARGET_COMMITISH": "a" * 40,
"DRAFT": json.dumps(draft), "PRERELEASE": json.dumps(prerelease),
"DIGEST": digest, "EXISTS": json.dumps(exists)}
result = subprocess.run(["bash", "-c", script], cwd=root, env=env,
capture_output=True, text=True)
calls = [json.loads(line) for line in (root / "calls.jsonl").read_text().splitlines()]
return result, calls
def test_draft_with_matching_asset_cannot_reach_pages(self):
result, calls = self.run_publication(draft=True)
self.assertNotEqual(result.returncode, 0)
self.assertNotIn("PAGES_BOUNDARY", result.stdout)
self.assertFalse(any(c[:2] in (["release", "edit"], ["release", "upload"],
["release", "create"]) for c in calls))
def test_draft_without_asset_is_not_modified(self):
result, calls = self.run_publication(draft=True, digest="")
self.assertNotEqual(result.returncode, 0)
self.assertEqual(len(calls), 1)
def test_unknown_draft_state_fails_closed(self):
result, _ = self.run_publication(draft=None)
self.assertNotEqual(result.returncode, 0)
self.assertNotIn("PAGES_BOUNDARY", result.stdout)
def test_published_matching_chart_retry_is_read_only(self):
result, calls = self.run_publication()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("PAGES_BOUNDARY", result.stdout)
self.assertEqual(len(calls), 2)
def test_published_mismatched_chart_is_not_replaced(self):
result, calls = self.run_publication(digest="sha256:" + "c" * 64)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(len(calls), 2)
self.assertNotIn("PAGES_BOUNDARY", result.stdout)
def test_published_stable_classification_is_corrected(self):
result, calls = self.run_publication(prerelease=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(calls[-1][:2], ["release", "edit"])
self.assertIn("--prerelease=false", calls[-1])
self.assertIn("--latest=false", calls[-1])
def test_new_stable_and_preview_classification(self):
for version, expected in (("6.4.3", "false"), ("6.4.3-rc.2", "true")):
with self.subTest(version=version):
result, calls = self.run_publication(exists=False, version=version)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(calls[-1][:2], ["release", "create"])
self.assertIn(f"--prerelease={expected}", calls[-1])
self.assertIn("--latest=false", calls[-1])
if __name__ == "__main__":
unittest.main()
@@ -16,6 +16,7 @@ import yaml
from yaml.constructor import ConstructorError
import record_rc_to_ga_blocked as blocked_record
import helm_pages_retry_test
from live_runtime_proof import evaluate_live_runtime
from release_promotion_policy_support import (
REQUIRED_STAGED_GOVERNANCE_INPUTS,
@@ -581,6 +582,20 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("--json isDraft,publishedAt,tagName", floating_tags)
self.assertIn("Floating-tag promotion refuses inactive release", floating_tags)
def test_helm_pages_retry_requires_explicitly_published_chart(self) -> None:
runner = helm_pages_retry_test.HelmPagesRetryTests()
for draft, digest in ((True, helm_pages_retry_test.DIGEST), (True, ""),
(None, helm_pages_retry_test.DIGEST)):
with self.subTest(draft=draft, digest=digest):
result, calls = runner.run_publication(draft=draft, digest=digest)
self.assertNotEqual(result.returncode, 0)
self.assertNotIn("PAGES_BOUNDARY", result.stdout)
self.assertEqual(len(calls), 1, "draft refusal must precede any mutation")
result, calls = runner.run_publication(draft=False)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("PAGES_BOUNDARY", result.stdout)
self.assertEqual(len(calls), 2, "matching published chart retry is read-only")
def test_each_post_commit_surface_failure_is_retriable_convergence_debt(self) -> None:
release_workflow = read(".github/workflows/create-release.yml")
convergence = read(".github/workflows/release-convergence.yml")