mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user