mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 21:47:20 +00:00
99f72ffedb
Replace unsafe generic update and historical downgrade shortcuts with the existing deployment-owned procedures. Keep configuration export distinct from full-state recovery and preserve data, keys, edition, uncertain-attempt evidence and ordinary monitoring checks. Change-source: pulse-maintainer
3650 lines
206 KiB
Python
3650 lines
206 KiB
Python
#!/usr/bin/env python3
|
|
"""Guard the RC-to-GA promotion policy across docs and release workflows."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path, PurePosixPath
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
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,
|
|
promotion_metadata_envelope,
|
|
slice_requires_staged_governance_inputs,
|
|
staged_governance_input_errors,
|
|
)
|
|
from repo_file_io import REPO_ROOT, git_env, read_repo_text, strip_local_git_env
|
|
|
|
USE_STAGED_GOVERNANCE = os.environ.get("PULSE_READ_STAGED_GOVERNANCE") == "1"
|
|
|
|
|
|
def read(rel: str) -> str:
|
|
return read_repo_text(
|
|
rel,
|
|
staged=USE_STAGED_GOVERNANCE,
|
|
strict_staged=USE_STAGED_GOVERNANCE and rel in REQUIRED_STAGED_GOVERNANCE_INPUTS,
|
|
)
|
|
|
|
|
|
def read_json(rel: str) -> dict:
|
|
return json.loads(read(rel))
|
|
|
|
|
|
def normalize_ws(text: str) -> str:
|
|
return " ".join(text.split())
|
|
|
|
|
|
def expandable_intake_context(form: dict, field_id: str, heading: str) -> str:
|
|
"""Read the conditional guidance at its report field, not arbitrary prose.
|
|
|
|
The forms keep optional context in expandable Markdown immediately before
|
|
the relevant textarea. A matching sentence elsewhere must not conceal a
|
|
removed, misplaced or malformed section.
|
|
"""
|
|
fields = [index for index, field in enumerate(form["body"]) if field.get("id") == field_id]
|
|
if len(fields) != 1 or fields[0] == 0:
|
|
raise ValueError("missing or repeated report field")
|
|
block = form["body"][fields[0] - 1]
|
|
if block.get("type") != "markdown":
|
|
raise ValueError("conditional context must precede its report field as Markdown")
|
|
text = block.get("attributes", {}).get("value", "")
|
|
sections = re.findall(r"<details>\s*<summary>([^<]+)</summary>(.*?)</details>", text, re.DOTALL)
|
|
if text.count("<details>") != len(sections) or text.count("</details>") != len(sections):
|
|
raise ValueError("malformed conditional context")
|
|
matches = [context for title, context in sections if title == heading]
|
|
if len(matches) != 1:
|
|
raise ValueError("missing, malformed or repeated conditional context")
|
|
return normalize_ws(matches[0])
|
|
|
|
|
|
def release_notes_have_exact_title(notes: str, version: str) -> bool:
|
|
"""Match the first title, as the reader-facing Go notes contract does.
|
|
|
|
The optional legacy suffix is presentation, not version identity. Neither
|
|
a later heading nor a version repeated in prose establishes that identity.
|
|
"""
|
|
first = notes.strip().split("\n", 1)[0].strip()
|
|
return re.fullmatch(r"# Pulse v" + re.escape(version) + r"(?: Release Notes)?", first) is not None
|
|
|
|
|
|
_MATERIAL_APPROVAL_RE = re.compile(
|
|
r"(?i)(?:"
|
|
r"\brichard[- ]approved\b|"
|
|
r"\bapproved by (?:richard|the (?:project|product|release) owner)\b|"
|
|
r"\b(?:project|product|release) owner (?:then )?(?:explicitly )?approved\b"
|
|
r")"
|
|
)
|
|
_QUANTITATIVE_RATIONALE_RE = re.compile(
|
|
r"(?i)(?:"
|
|
r"\d+(?:\.\d+)?\s*%|"
|
|
r"\bconversion(?: rate)?\b|"
|
|
r"\bweekly active\b|"
|
|
r"\bsubscriptions?\s*(?:/|per )\s*(?:month|week|year)\b|"
|
|
r"\bgrew from\b|"
|
|
r"\bmeasured on\b|"
|
|
r"\bcohort(?:s)?\b"
|
|
r")"
|
|
)
|
|
_QUANTITATIVE_EVIDENCE_FIELDS = (
|
|
"Source",
|
|
"Query",
|
|
"Snapshot",
|
|
"Measured at",
|
|
)
|
|
_UNUSABLE_EVIDENCE_VALUE_RE = re.compile(
|
|
r"(?i)^(?:none|n/a|na|unknown|unavailable|not (?:available|recorded)|missing)$"
|
|
)
|
|
_INLINE_EVIDENCE_REFERENCE_RE = re.compile(r"`([^`]+)`")
|
|
_RFC3339_UTC_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$")
|
|
|
|
|
|
def _heading_anchor(heading: str) -> str:
|
|
normalized = re.sub(r"[^a-z0-9 _-]", "", heading.strip().lower())
|
|
return re.sub(r"[ _]+", "-", normalized)
|
|
|
|
|
|
def _local_evidence_reference_error(
|
|
*, field: str, value: str, record_rel: str
|
|
) -> str | None:
|
|
reference_match = _INLINE_EVIDENCE_REFERENCE_RE.search(value)
|
|
if reference_match is None:
|
|
return f"'- {field}:' must contain an inline durable reference"
|
|
|
|
reference = reference_match.group(1).strip()
|
|
if reference.startswith(("https://", "http://")):
|
|
return None
|
|
if field == "Source" and re.fullmatch(r"[A-Za-z0-9_.-]+@[0-9a-f]{7,40}:.+", reference):
|
|
return None
|
|
|
|
path_part, separator, anchor = reference.partition("#")
|
|
if not path_part or path_part.startswith("/") or any(char.isspace() for char in path_part):
|
|
return f"'- {field}:' must reference a repository artifact, source identifier, or URL"
|
|
|
|
candidate = PurePosixPath(path_part)
|
|
if candidate.parts and candidate.parts[0] not in {"docs", "scripts"}:
|
|
candidate = PurePosixPath(record_rel).parent / candidate
|
|
candidate_rel = candidate.as_posix()
|
|
try:
|
|
target_content = read(candidate_rel)
|
|
except (OSError, subprocess.CalledProcessError):
|
|
return f"'- {field}:' references missing artifact {candidate_rel!r}"
|
|
|
|
if separator:
|
|
anchors = {
|
|
_heading_anchor(match.group(1))
|
|
for match in re.finditer(r"(?m)^#{1,6}\s+(.+?)\s*$", target_content)
|
|
}
|
|
if anchor not in anchors:
|
|
return f"'- {field}:' references missing section #{anchor} in {candidate_rel!r}"
|
|
return None
|
|
|
|
|
|
def material_approval_evidence_errors(
|
|
content: str, *, record_rel: str | None = None
|
|
) -> tuple[str, ...]:
|
|
"""Return structural evidence errors for quantitatively justified owner approvals."""
|
|
if not _MATERIAL_APPROVAL_RE.search(content):
|
|
return ()
|
|
if not _QUANTITATIVE_RATIONALE_RE.search(content):
|
|
return ()
|
|
|
|
errors: list[str] = []
|
|
if not re.search(r"(?m)^## Quantitative Evidence\s*$", content):
|
|
return ("missing '## Quantitative Evidence' section",)
|
|
|
|
values: dict[str, str] = {}
|
|
for field in _QUANTITATIVE_EVIDENCE_FIELDS:
|
|
match = re.search(rf"(?m)^- {re.escape(field)}:\s*(.+?)\s*$", content)
|
|
if match is None:
|
|
errors.append(f"missing '- {field}:' provenance field")
|
|
continue
|
|
value = match.group(1).strip().strip("`").strip()
|
|
values[field] = match.group(1).strip()
|
|
if not value or _UNUSABLE_EVIDENCE_VALUE_RE.fullmatch(value):
|
|
errors.append(f"'- {field}:' must identify durable reproducible evidence")
|
|
|
|
for field in ("Source", "Query", "Snapshot"):
|
|
value = values.get(field)
|
|
if value is None or _UNUSABLE_EVIDENCE_VALUE_RE.fullmatch(value.strip("`").strip()):
|
|
continue
|
|
if record_rel is None:
|
|
if _INLINE_EVIDENCE_REFERENCE_RE.search(value) is None:
|
|
errors.append(f"'- {field}:' must contain an inline durable reference")
|
|
continue
|
|
error = _local_evidence_reference_error(
|
|
field=field,
|
|
value=value,
|
|
record_rel=record_rel,
|
|
)
|
|
if error is not None:
|
|
errors.append(error)
|
|
|
|
measured_at = values.get("Measured at", "").strip("`").strip()
|
|
if measured_at and not _UNUSABLE_EVIDENCE_VALUE_RE.fullmatch(measured_at):
|
|
if _RFC3339_UTC_RE.fullmatch(measured_at) is None:
|
|
errors.append("'- Measured at:' must be an exact RFC3339 UTC timestamp")
|
|
return tuple(errors)
|
|
|
|
|
|
def tracked_release_control_records() -> tuple[str, ...]:
|
|
result = subprocess.run(
|
|
[
|
|
"git",
|
|
"ls-files",
|
|
"docs/release-control/v6/internal/records/*.md",
|
|
],
|
|
cwd=REPO_ROOT,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
env=git_env(),
|
|
)
|
|
return tuple(sorted(line for line in result.stdout.splitlines() if line.strip()))
|
|
|
|
|
|
def workflow_job_block(workflow: str, job: str) -> str:
|
|
match = re.search(
|
|
rf"(?ms)^ {re.escape(job)}:\n.*?(?=^ [A-Za-z0-9_]+:\n|\Z)",
|
|
workflow,
|
|
)
|
|
if match is None:
|
|
raise AssertionError(f"workflow missing job {job}")
|
|
return match.group(0)
|
|
|
|
|
|
class UniqueKeyLoader(yaml.SafeLoader):
|
|
"""YAML loader that fails instead of silently overwriting duplicate keys."""
|
|
|
|
|
|
def _construct_unique_mapping(loader: UniqueKeyLoader, node: yaml.MappingNode, deep: bool = False) -> dict:
|
|
seen: set[object] = set()
|
|
for key_node, _ in node.value:
|
|
key = loader.construct_object(key_node, deep=deep)
|
|
if key in seen:
|
|
raise ConstructorError(
|
|
"while constructing a mapping",
|
|
node.start_mark,
|
|
f"found duplicate key {key!r}",
|
|
key_node.start_mark,
|
|
)
|
|
seen.add(key)
|
|
return yaml.SafeLoader.construct_mapping(loader, node, deep=deep)
|
|
|
|
|
|
UniqueKeyLoader.add_constructor(
|
|
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
|
|
_construct_unique_mapping,
|
|
)
|
|
|
|
|
|
_RC_DRAFT_PACKET_NAME_RE = re.compile(r"^RELEASE_NOTES_v6_RC(\d+)_DRAFT\.md$")
|
|
|
|
|
|
def discover_rc_draft_packets() -> tuple[tuple[int, str, str, str], ...]:
|
|
"""Return (rc_number, release_notes, changelog, support_pack) for every in-repo RC draft packet, sorted by RC.
|
|
|
|
Driven from the filesystem so adding a new RC packet automatically extends
|
|
every test that loops over the discovered set; nobody has to edit hardcoded
|
|
dict entries per RC.
|
|
"""
|
|
packets: list[tuple[int, str, str, str]] = []
|
|
releases_dir = REPO_ROOT / "docs" / "releases"
|
|
for path in sorted(releases_dir.glob("RELEASE_NOTES_v6_RC*_DRAFT.md")):
|
|
match = _RC_DRAFT_PACKET_NAME_RE.match(path.name)
|
|
if not match:
|
|
continue
|
|
n = int(match.group(1))
|
|
packets.append(
|
|
(
|
|
n,
|
|
f"docs/releases/RELEASE_NOTES_v6_RC{n}_DRAFT.md",
|
|
f"docs/releases/V6_CHANGELOG_RC{n}_DRAFT.md",
|
|
f"docs/releases/V6_RC{n}_OPERATOR_SUPPORT_PACK_DRAFT.md",
|
|
)
|
|
)
|
|
return tuple(sorted(packets))
|
|
|
|
|
|
def rc_packet_paths_for_version(version: str) -> tuple[str, str, str] | None:
|
|
"""Return the (release_notes, changelog, support_pack) draft paths for a 6.0.0-rc.N VERSION, or None otherwise."""
|
|
match = re.match(r"^6\.0\.0-rc\.(\d+)$", version)
|
|
if not match:
|
|
return None
|
|
n = int(match.group(1))
|
|
return (
|
|
f"docs/releases/RELEASE_NOTES_v6_RC{n}_DRAFT.md",
|
|
f"docs/releases/V6_CHANGELOG_RC{n}_DRAFT.md",
|
|
f"docs/releases/V6_RC{n}_OPERATOR_SUPPORT_PACK_DRAFT.md",
|
|
)
|
|
|
|
|
|
def support_prerelease_packet_paths_for_version(version: str) -> tuple[str, str] | None:
|
|
"""Return release-notes and changelog paths for post-GA support beta and RC versions."""
|
|
if not re.match(r"^6\.\d+\.\d+-(?:rc|beta)\.\d+$", version):
|
|
return None
|
|
if version.startswith("6.0.0-rc."):
|
|
return None
|
|
return (
|
|
f"docs/releases/RELEASE_NOTES_v{version}.md",
|
|
f"docs/releases/V6_CHANGELOG_v{version}.md",
|
|
)
|
|
|
|
|
|
def stable_packet_paths_for_version(version: str) -> tuple[str, str] | None:
|
|
"""Return the stable release-notes and changelog packet paths for a v6 stable VERSION."""
|
|
if not re.match(r"^6\.\d+\.\d+$", version):
|
|
return None
|
|
if version == "6.0.0":
|
|
return ("docs/releases/RELEASE_NOTES_v6.md", "docs/releases/V6_CHANGELOG.md")
|
|
return (
|
|
f"docs/releases/RELEASE_NOTES_v{version}.md",
|
|
f"docs/releases/V6_CHANGELOG_v{version}.md",
|
|
)
|
|
|
|
|
|
def staged_files() -> tuple[str, ...]:
|
|
result = subprocess.run(
|
|
["git", "diff", "--cached", "--name-only"],
|
|
cwd=REPO_ROOT,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
env=git_env(),
|
|
)
|
|
return tuple(line for line in result.stdout.splitlines() if line.strip())
|
|
|
|
|
|
STAGED_FILES = staged_files() if USE_STAGED_GOVERNANCE else ()
|
|
REQUIRES_STAGED_GOVERNANCE_INPUTS = slice_requires_staged_governance_inputs(STAGED_FILES)
|
|
STAGED_GOVERNANCE_INPUT_ERRORS = (
|
|
tuple(staged_governance_input_errors(use_staged_governance=True))
|
|
if REQUIRES_STAGED_GOVERNANCE_INPUTS
|
|
else ()
|
|
)
|
|
|
|
|
|
class ReleasePromotionPolicyTest(unittest.TestCase):
|
|
|
|
def test_quarantined_draft_identity_before_patch(self):
|
|
workflow = yaml.safe_load(read(".github/workflows/create-release.yml"))
|
|
steps = workflow["jobs"]["create_release"]["steps"]
|
|
create = next(step for step in steps if step.get("name") == "Create draft release")
|
|
locate = next(step for step in steps if step.get("id") == "existing_release")
|
|
self.assertIn(".target_commitish // empty", locate["run"])
|
|
self.assertIn('write_github_output.py release_target_commitish "${RELEASE_TARGET_COMMITISH}"',
|
|
locate["run"])
|
|
self.assertEqual(create["env"]["WORKFLOW_OUTPUT_10"],
|
|
"${{ steps.existing_release.outputs.release_target_commitish }}")
|
|
check = next(step for step in steps
|
|
if step.get("name") == "Check existing public tag without changing it")
|
|
self.assertLess(steps.index(check), steps.index(create))
|
|
script = (check["run"] + "\n" + create["run"]).replace(
|
|
"${{ github.repository }}", "fixture/pulse")
|
|
# Execute the actual draft shell with an absent-tag fake Git and a
|
|
# recording API sentinel. No network or public writer is available.
|
|
head = "a" * 40
|
|
cases = [
|
|
("different", "b" * 40, "2026-09-01T00:00:00Z", "true", "false", False),
|
|
("unknown", "", "2026-09-01T00:00:00Z", "true", "false", False),
|
|
("branch", "main", "2026-09-01T00:00:00Z", "true", "false", False),
|
|
("same", head, "2026-09-01T00:00:00Z", "true", "false", True),
|
|
("private", "b" * 40, "", "true", "false", True),
|
|
("activated", head, "2026-09-01T00:00:00Z", "true", "true", False),
|
|
("published", head, "2026-09-01T00:00:00Z", "false", "false", False),
|
|
]
|
|
for label, target, published, draft, activated, allowed in cases:
|
|
with self.subTest(label=label), tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
(root / "notes").write_text("Fixture notes")
|
|
(root / "git").write_text(
|
|
"#!/bin/bash\n"
|
|
'if [ "$*" = "rev-parse HEAD" ]; then echo ' + head + '; exit 0; fi\n'
|
|
'if [ "$1" = "ls-remote" ]; then exit 0; fi\nexit 98\n'
|
|
)
|
|
(root / "gh").write_text(
|
|
"#!/bin/bash\nprintf '%s\\n' \"$*\" >> \"$CALLS\"\nexit 73\n"
|
|
)
|
|
for name in ("git", "gh"):
|
|
(root / name).chmod(0o755)
|
|
env = dict(os.environ, PATH=f"{root}:/usr/bin:/bin", TMPDIR=tmp,
|
|
CALLS=str(root / "calls"), TAG="v6.4.4-beta.4")
|
|
values = ["v6.4.4-beta.4", str(root / "notes"), "true", "123",
|
|
"https://example.invalid/release", draft, published, activated,
|
|
"6.4.4-beta.4", target]
|
|
env.update({f"WORKFLOW_OUTPUT_{i}": v for i, v in enumerate(values, 1)})
|
|
result = subprocess.run(["bash", "-euo", "pipefail", "-c", script],
|
|
cwd=root, env=env, text=True, capture_output=True)
|
|
calls = (root / "calls").read_text() if (root / "calls").exists() else ""
|
|
if allowed:
|
|
self.assertEqual(result.returncode, 73, result.stdout + result.stderr)
|
|
self.assertIn("-X PATCH", calls)
|
|
else:
|
|
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
self.assertEqual(calls, "", "refusal must precede API mutation")
|
|
|
|
def test_reviewed_action_manifests_cover_all_release_consumers(self) -> None:
|
|
# Snapshot is derived from each immutable upstream action.yml, not from
|
|
# our consumers: unknown inputs therefore fail rather than being blessed.
|
|
manifests = read_json("scripts/release_control/action_consumer_manifests.json")
|
|
seen = set()
|
|
for path in (REPO_ROOT / ".github/workflows").glob("*.yml"):
|
|
workflow = yaml.safe_load(read(str(path.relative_to(REPO_ROOT))))
|
|
for job in workflow.get("jobs", {}).values():
|
|
for step in job.get("steps", []):
|
|
action, _, revision = step.get("uses", "").partition("@")
|
|
if action not in manifests:
|
|
continue
|
|
with self.subTest(workflow=path.name, action=action):
|
|
manifest = manifests[action]
|
|
self.assertEqual(revision, manifest["sha"])
|
|
self.assertEqual(manifest["runs"]["using"], "node24")
|
|
self.assertRegex(manifest["manifest_sha256"], r"^[0-9a-f]{64}$")
|
|
self.assertLessEqual(set(step.get("with", {})), set(manifest["inputs"]))
|
|
seen.add(action)
|
|
if action == "azure/setup-helm":
|
|
self.assertEqual(str(step["with"]["version"]).lstrip("v"), "3.15.2")
|
|
if action == "actions/attest":
|
|
if path.name in {"publish-docker.yml", "publish-helm-chart.yml"}:
|
|
self.assertIn("subject-name", step["with"])
|
|
self.assertIn("subject-digest", step["with"])
|
|
self.assertTrue(step["with"]["push-to-registry"])
|
|
else:
|
|
self.assertIn("subject-checksums", step["with"])
|
|
if action == "signpath/github-action-submit-signing-request":
|
|
self.assertIn("api-token", step["with"])
|
|
self.assertIn("organization-id", step["with"])
|
|
self.assertEqual(seen, set(manifests))
|
|
|
|
def setUp(self) -> None:
|
|
if USE_STAGED_GOVERNANCE and not REQUIRES_STAGED_GOVERNANCE_INPUTS:
|
|
self.skipTest("staged slice does not touch the promotion-proof surface")
|
|
if (
|
|
STAGED_GOVERNANCE_INPUT_ERRORS
|
|
and self._testMethodName != "test_staged_governance_inputs_are_present"
|
|
):
|
|
self.skipTest("staged governance inputs missing; see test_staged_governance_inputs_are_present")
|
|
|
|
def test_scheduled_rehearsal_preserves_source_identity_boundary(self) -> None:
|
|
workflow = read(".github/workflows/release-dry-run.yml")
|
|
jobs = yaml.safe_load(workflow)["jobs"]
|
|
dry_run = jobs["dry-run"]
|
|
steps = dry_run["steps"]
|
|
names = [step["name"] for step in steps]
|
|
self.assertLess(names.index("Validate release ref"), names.index("Checkout repository"))
|
|
self.assertLess(names.index("Resolve required release branch"),
|
|
names.index("Select exact rehearsal source"))
|
|
self.assertLess(names.index("Select exact rehearsal source"),
|
|
names.index("Resolve rehearsal metadata"))
|
|
source = next(step for step in steps if step.get("id") == "source")
|
|
self.assertEqual(source["env"]["REQUIRED_BRANCH"],
|
|
"${{ steps.branch_policy.outputs.required_branch }}")
|
|
self.assertEqual(source["env"]["WATCHDOG_MODE"], "${{ steps.mode.outputs.watchdog }}")
|
|
self.assertIn('git checkout --detach "${TESTED_SHA}"', source["run"])
|
|
self.assertIn('"${TESTED_SHA}" != "${GITHUB_SHA}"', source["run"])
|
|
metadata = next(step for step in steps if step.get("id") == "rehearsal")
|
|
self.assertEqual(metadata["env"]["TESTED_BRANCH"],
|
|
"${{ steps.source.outputs.tested_branch }}")
|
|
self.assertIn('if [ "${TESTED_BRANCH}" != "$REQUIRED_BRANCH" ]; then',
|
|
metadata["run"])
|
|
self.assertIn('if [ "$FILE_VERSION" != "$VERSION" ]; then', metadata["run"])
|
|
self.assertIn(
|
|
'if [ "${WATCHDOG_MODE}" = "true" ]; then',
|
|
metadata["run"],
|
|
)
|
|
self.assertIn('metadata_mode=watchdog', metadata["run"])
|
|
self.assertIn('metadata_mode=promotion', metadata["run"])
|
|
self.assertEqual(metadata["env"]["WATCHDOG_MODE"], "${{ steps.mode.outputs.watchdog }}")
|
|
self.assertEqual(jobs["build_release_candidate"]["if"],
|
|
"${{ inputs.watchdog != true && inputs.version != '' }}")
|
|
self.assertIn("Release Watchdog at {0}", yaml.safe_load(workflow)["run-name"])
|
|
for key in ("tested_sha", "tested_branch"):
|
|
self.assertEqual(dry_run["outputs"][key],
|
|
"${{ steps.source.outputs." + key + " }}")
|
|
verdict = jobs["release_dry_run_verdict"]["steps"][0]
|
|
self.assertEqual(verdict["env"]["TESTED_SHA"],
|
|
"${{ needs.dry-run.outputs.tested_sha }}")
|
|
self.assertIn("Workflow event SHA:", verdict["run"])
|
|
self.assertIn("Tested source:", verdict["run"])
|
|
self.assertNotIn('echo "- Source SHA:', verdict["run"])
|
|
|
|
def test_release_workflow_supports_reviewed_immutable_snapshots(self) -> None:
|
|
from release_snapshot import check_workflow
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
workflow = Path(directory) / "create-release.yml"
|
|
workflow.write_text(read(".github/workflows/create-release.yml"))
|
|
check_workflow(workflow)
|
|
|
|
def test_staged_governance_inputs_are_present(self) -> None:
|
|
if STAGED_GOVERNANCE_INPUT_ERRORS:
|
|
self.fail(
|
|
"staged promotion proof inputs are incomplete:\n- "
|
|
+ "\n- ".join(STAGED_GOVERNANCE_INPUT_ERRORS)
|
|
)
|
|
|
|
def test_release_workflows_reject_duplicate_yaml_keys(self) -> None:
|
|
for workflow_path in (
|
|
".github/workflows/create-release.yml",
|
|
".github/workflows/release-convergence.yml",
|
|
".github/workflows/retry-release-convergence.yml",
|
|
".github/workflows/promote-private-pro-runtime.yml",
|
|
".github/workflows/promote-floating-tags.yml",
|
|
".github/workflows/helm-pages.yml",
|
|
".github/workflows/update-demo-server.yml",
|
|
".github/workflows/deploy-demo-server.yml",
|
|
):
|
|
with self.subTest(workflow_path=workflow_path):
|
|
yaml.load(read(workflow_path), Loader=UniqueKeyLoader)
|
|
|
|
def test_customer_promotion_lease_helper_is_tracked_and_executable(self) -> None:
|
|
helper = REPO_ROOT / "scripts" / "release_control" / "customer_promotion_lease.sh"
|
|
tracked = subprocess.run(
|
|
["git", "ls-files", "--error-unmatch", str(helper.relative_to(REPO_ROOT))],
|
|
cwd=REPO_ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
self.assertEqual(tracked.returncode, 0, tracked.stderr)
|
|
self.assertTrue(os.access(helper, os.X_OK), f"{helper} must be executable")
|
|
convergence = read(".github/workflows/release-convergence.yml")
|
|
self.assertIn("scripts/release_control/customer_promotion_lease.sh acquire", convergence)
|
|
self.assertIn("scripts/release_control/customer_promotion_lease.sh release", convergence)
|
|
|
|
def test_release_commit_dispatches_durable_convergence_before_activation(self) -> None:
|
|
workflow = read(".github/workflows/create-release.yml")
|
|
convergence = read(".github/workflows/release-convergence.yml")
|
|
publication_preflight = workflow_job_block(
|
|
workflow, "publication_trust_preflight"
|
|
)
|
|
dispatch = workflow_job_block(workflow, "dispatch_release_convergence")
|
|
activation = workflow_job_block(workflow, "activate_release")
|
|
commit_verdict = workflow_job_block(workflow, "release_commit_verdict")
|
|
recovery = read(".github/workflows/recover-release-activation.yml")
|
|
recovery_activation = workflow_job_block(recovery, "recover_activation")
|
|
|
|
self.assertIn("runs-on: ubuntu-24.04", publication_preflight)
|
|
self.assertIn("secrets.WORKFLOW_PAT", publication_preflight)
|
|
self.assertIn(
|
|
"check-github-release-immutability.sh", publication_preflight
|
|
)
|
|
self.assertIn("github.event.inputs.draft_only != 'true'", publication_preflight)
|
|
self.assertIn(
|
|
"historical_asset_backfill_only != 'true'", publication_preflight
|
|
)
|
|
for early_job_name in (
|
|
"build_release_candidate",
|
|
"frontend_bundle",
|
|
"frontend_checks",
|
|
"windows_install_command_smoke",
|
|
"release_note_visuals",
|
|
"stage_private_pro_runtime",
|
|
):
|
|
with self.subTest(early_job_name=early_job_name):
|
|
early_job = workflow_job_block(workflow, early_job_name)
|
|
self.assertIn("- publication_trust_preflight", early_job)
|
|
|
|
# The echo-only joins are gone: activation joins the tag and registry
|
|
# publications directly.
|
|
self.assertNotRegex(workflow, r"(?m)^ (candidate_qualification|release_readiness):$")
|
|
for dependency in ("publish_release_tag", "publish_docker", "publish_helm_chart"):
|
|
self.assertIn(f"- {dependency}", activation)
|
|
self.assertIn(f"needs.{dependency}.result == 'success'", activation)
|
|
for mutable_job in (
|
|
"publish_helm_pages",
|
|
"promote_floating_tags",
|
|
"promote_private_pro_runtime",
|
|
"update_stable_demo",
|
|
):
|
|
with self.subTest(mutable_job=mutable_job):
|
|
self.assertNotIn(f"- {mutable_job}", activation)
|
|
self.assertNotRegex(workflow, rf"(?m)^ {mutable_job}:$")
|
|
mutable = workflow_job_block(convergence, mutable_job)
|
|
self.assertIn("needs: acquire_customer_promotion_lease", mutable)
|
|
|
|
self.assertIn(
|
|
"needs.publication_trust_preflight.result == 'success'",
|
|
workflow_job_block(workflow, "publish_release_tag")
|
|
)
|
|
self.assertIn("- publication_trust_preflight", commit_verdict)
|
|
self.assertIn(
|
|
'require_result "publication trust preflight"', commit_verdict
|
|
)
|
|
self.assertIn("- dispatch_release_convergence", activation)
|
|
self.assertNotIn("- publish_release_tag", dispatch)
|
|
self.assertIn("- create_release", dispatch)
|
|
self.assertIn("- stage_private_pro_runtime", dispatch)
|
|
self.assertIn("github.event.inputs.draft_only != 'true'", dispatch)
|
|
self.assertIn("needs.prepare.outputs.historical_asset_backfill_only != 'true'", dispatch)
|
|
self.assertIn("return_run_details: true", dispatch)
|
|
self.assertIn("release-convergence.yml/dispatches", dispatch)
|
|
self.assertIn("Customer convergence dispatch did not return an exact workflow run", dispatch)
|
|
self.assertIn("continue-on-error: true", activation)
|
|
self.assertIn("returning ${TAG} to draft quarantine", activation)
|
|
self.assertIn("Resuming quarantined activation for ${TAG}", activation)
|
|
self.assertNotIn('[ -n "$published_at" ] ||', activation)
|
|
self.assertIn('[ "$activation_committed" = "true" ] ||', activation)
|
|
self.assertIn('committed=false', activation)
|
|
self.assertIn('[ "$committed" != "true" ]', activation)
|
|
self.assertIn("release-activation.json", activation)
|
|
self.assertIn("r2_prefix: $r2_prefix", activation)
|
|
self.assertIn(".r2_prefix == $r2_prefix", activation)
|
|
self.assertIn(".r2_prefix == $r2_prefix", convergence)
|
|
for digest_field in (
|
|
"server_image_digest",
|
|
"control_plane_image_digest",
|
|
"helm_chart_digest",
|
|
):
|
|
self.assertIn(digest_field, activation)
|
|
self.assertIn(digest_field, convergence)
|
|
self.assertIn(digest_field, recovery_activation)
|
|
self.assertIn(digest_field, commit_verdict)
|
|
self.assertIn("committed=true", activation)
|
|
self.assertIn("Immutably committed, attested, and publicly verified ${TAG}", activation)
|
|
self.assertIn("Draft activation marker digest does not match", activation)
|
|
self.assertIn(".immutable // false", activation)
|
|
self.assertIn("verify-github-release-integrity.sh", activation)
|
|
self.assertIn("verify-github-release-integrity.sh", convergence)
|
|
self.assertIn("verify-github-release-integrity.sh", commit_verdict)
|
|
self.assertIn('"${EXPECTED_COMMIT}" "${marker}"', convergence)
|
|
self.assertIn('"${verified_marker}"', activation)
|
|
marker_download = convergence.index('-o "${marker}"')
|
|
marker_integrity = convergence.index(
|
|
"verify-github-release-integrity.sh", marker_download
|
|
)
|
|
marker_parse = convergence.index("if ! jq -e", marker_download)
|
|
self.assertLess(marker_download, marker_integrity)
|
|
self.assertLess(marker_integrity, marker_parse)
|
|
for publication_job in (activation, recovery_activation):
|
|
with self.subTest(publication_job=publication_job[:40]):
|
|
self.assertIn(
|
|
"IMMUTABILITY_ADMIN_TOKEN: ${{ secrets.WORKFLOW_PAT }}",
|
|
publication_job,
|
|
)
|
|
self.assertIn("check-github-release-immutability.sh", publication_job)
|
|
setting_check = publication_job.index(
|
|
"check-github-release-immutability.sh"
|
|
)
|
|
self.assertLess(
|
|
setting_check,
|
|
publication_job.index('-X PATCH --input', setting_check),
|
|
)
|
|
marker_upload = activation.index('gh release upload "${TAG}"')
|
|
publish_patch = activation.index(
|
|
'-X PATCH --input "$publish_payload"', marker_upload
|
|
)
|
|
commit_flip = activation.index("committed=true", publish_patch)
|
|
activation_readback = activation.index("curl -fsSL --retry 12", commit_flip)
|
|
self.assertIn(
|
|
'--repo "${GITHUB_REPOSITORY}"',
|
|
activation[marker_upload:publish_patch],
|
|
)
|
|
self.assertLess(marker_upload, publish_patch)
|
|
self.assertLess(publish_patch, commit_flip)
|
|
self.assertLess(commit_flip, activation_readback)
|
|
|
|
# Failure injection: the draft marker is still compensatable, but once
|
|
# immutable publication succeeds, a later public read-back failure may
|
|
# not attempt to mutate the locked release.
|
|
state = {"activated": False, "marker_staged": True, "committed": False}
|
|
should_remove_marker = state["marker_staged"] and not state["committed"]
|
|
self.assertTrue(should_remove_marker)
|
|
state["activated"] = True
|
|
state["committed"] = True
|
|
activation_readback_succeeded = False
|
|
should_quarantine = state["activated"] and not state["committed"]
|
|
self.assertFalse(activation_readback_succeeded)
|
|
self.assertFalse(should_quarantine)
|
|
self.assertIn("Release Activation Commit Verdict", commit_verdict)
|
|
self.assertIn("release-activation.json", commit_verdict)
|
|
for forbidden in (
|
|
"FLOATING_RESULT",
|
|
"HELM_PAGES_RESULT",
|
|
"PRIVATE_PRO_PROMOTION_RESULT",
|
|
"DEMO_RESULT",
|
|
):
|
|
self.assertNotIn(forbidden, commit_verdict)
|
|
|
|
demo = workflow_job_block(convergence, "update_stable_demo")
|
|
self.assertNotIn("release_id:", demo)
|
|
demo_workflow = read(".github/workflows/update-demo-server.yml")
|
|
self.assertNotIn("release_id:", demo_workflow)
|
|
self.assertNotIn("unpublished draft", demo_workflow)
|
|
|
|
def test_activation_recovery_accepts_runs_with_and_without_readiness_join(self) -> None:
|
|
# Execute the recovery's source-job qualification against job listings
|
|
# in both create-release shapes. Old-shape runs must recover exactly as
|
|
# before; joinless runs must prove the publication jobs the join used
|
|
# to require. Job names mirror a real v6.4 release run.
|
|
job = workflow_job_block(read(".github/workflows/recover-release-activation.yml"), "recover_activation")
|
|
start = job.index("immutable_join=release_readiness")
|
|
end = job.index('gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100"')
|
|
script = "set -euo pipefail\n" + job[start:end] + "echo QUALIFIED\n"
|
|
common = [
|
|
("prepare", "success"), ("create_release", "success"),
|
|
("dispatch_release_convergence", "success"), ("backend_tests", "success"),
|
|
("integration_tests", "skipped"),
|
|
("publish_release_tag", "success"),
|
|
("publish_docker / Publish server image", "success"),
|
|
("publish_docker / Publish control-plane image", "success"),
|
|
("publish_docker / Verify exact image identities and provenance", "success"),
|
|
("publish_helm_chart / Package and Push Helm Chart", "success"),
|
|
("activate_release", "failure"),
|
|
("Release Activation Commit Verdict", "failure"),
|
|
]
|
|
|
|
def qualifies(jobs: list[tuple[str, str]]) -> bool:
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
listing = Path(temp) / "jobs.json"
|
|
listing.write_text(json.dumps([
|
|
{"name": name, "status": "completed", "conclusion": conclusion}
|
|
for name, conclusion in jobs
|
|
]))
|
|
result = subprocess.run(
|
|
["bash", "-c", script], env=os.environ | {"source_jobs": str(listing)},
|
|
text=True, capture_output=True,
|
|
)
|
|
self.assertEqual(result.returncode == 0, "QUALIFIED" in result.stdout, result.stderr)
|
|
return result.returncode == 0
|
|
|
|
def without(jobs: list[tuple[str, str]], prefix: str) -> list[tuple[str, str]]:
|
|
return [item for item in jobs if not item[0].startswith(prefix)]
|
|
|
|
def replaced(jobs: list[tuple[str, str]], name: str, conclusion: str) -> list[tuple[str, str]]:
|
|
return [(item[0], conclusion if item[0] == name else item[1]) for item in jobs]
|
|
|
|
old_shape = [("candidate_qualification", "success"), ("release_readiness", "success"), *common]
|
|
self.assertTrue(qualifies(old_shape))
|
|
# The old shape trusts only its join, as before, so reusable display
|
|
# names are never consulted for it.
|
|
self.assertTrue(qualifies(without(without(old_shape, "publish_docker"), "publish_helm_chart")))
|
|
self.assertFalse(qualifies(replaced(old_shape, "release_readiness", "skipped")))
|
|
self.assertFalse(qualifies(replaced(old_shape, "dispatch_release_convergence", "skipped")))
|
|
|
|
new_shape = common
|
|
self.assertTrue(qualifies(new_shape))
|
|
for name in ("prepare", "create_release", "dispatch_release_convergence", "publish_release_tag",
|
|
"publish_docker / Publish server image",
|
|
"publish_helm_chart / Package and Push Helm Chart"):
|
|
with self.subTest(skipped=name):
|
|
self.assertFalse(qualifies(replaced(new_shape, name, "skipped")))
|
|
for prefix in ("publish_release_tag", "publish_docker", "publish_helm_chart"):
|
|
with self.subTest(missing=prefix):
|
|
self.assertFalse(qualifies(without(new_shape, prefix)))
|
|
# A skipped reusable caller is listed under its bare job ID.
|
|
for name in ("publish_docker", "publish_helm_chart"):
|
|
with self.subTest(skipped_caller=name):
|
|
self.assertFalse(qualifies([*without(new_shape, name), (name, "skipped")]))
|
|
# Failures outside the activation boundary still reject either shape.
|
|
self.assertFalse(qualifies(replaced(new_shape, "backend_tests", "failure")))
|
|
self.assertFalse(qualifies(replaced(old_shape, "backend_tests", "failure")))
|
|
|
|
def test_commit_verdict_restates_the_candidate_predicate(self) -> None:
|
|
# The verdict no longer reads a readiness join, so it must reject every
|
|
# candidate failure itself. Run its result checks with each outcome.
|
|
verdict = workflow_job_block(read(".github/workflows/create-release.yml"), "release_commit_verdict")
|
|
step = yaml.safe_load("jobs:\n" + verdict)["jobs"]["release_commit_verdict"]["steps"][-1]
|
|
script = step["run"]
|
|
script = script[:script.index("./scripts/verify-github-release-integrity.sh")] + "exit 0\nfi\n"
|
|
results = {
|
|
"PUBLICATION_TRUST_RESULT", "SMOKE_RESULT", "WINDOWS_INSTALL_COMMAND_RESULT",
|
|
"CREATE_RESULT", "VALIDATE_RESULT", "DOCKER_RESULT", "INSTALL_RESULT", "HELM_RESULT",
|
|
"BUILD_CANDIDATE_RESULT", "CONTAINER_QUALIFICATION_RESULT", "FRONTEND_BUNDLE_RESULT",
|
|
"FRONTEND_CHECKS_RESULT", "BACKEND_RESULT", "INTEGRATION_RESULT", "TAG_RESULT",
|
|
"CONVERGENCE_DISPATCH_RESULT", "PRIVATE_PRO_STAGE_RESULT",
|
|
}
|
|
self.assertTrue(results <= set(step["env"]))
|
|
self.assertNotIn("READINESS_RESULT", step["env"])
|
|
good = dict.fromkeys(results, "success") | {"DRAFT_ONLY": "false", "VERSION": "6.6.0"}
|
|
|
|
def passes(env: dict[str, str]) -> bool:
|
|
result = subprocess.run(["bash", "-c", script], env=os.environ | env, text=True, capture_output=True)
|
|
return result.returncode == 0
|
|
|
|
self.assertTrue(passes(good))
|
|
self.assertTrue(passes(good | {"INTEGRATION_RESULT": "skipped"}))
|
|
for name in sorted(results):
|
|
for state in ("failure", "cancelled", "skipped"):
|
|
if name == "INTEGRATION_RESULT" and state == "skipped":
|
|
continue
|
|
with self.subTest(result=name, state=state):
|
|
self.assertFalse(passes(good | {name: state}))
|
|
|
|
def test_activation_recovery_reuses_the_qualified_candidate_without_rebuilding(self) -> None:
|
|
release = read(".github/workflows/create-release.yml")
|
|
recovery = read(".github/workflows/recover-release-activation.yml")
|
|
job = workflow_job_block(recovery, "recover_activation")
|
|
|
|
self.assertIn("group: release-v${{ github.event.inputs.version", release)
|
|
self.assertIn("group: release-${{ inputs.tag }}", recovery)
|
|
self.assertIn('.path == ".github/workflows/create-release.yml"', job)
|
|
self.assertIn("release_readiness", job)
|
|
self.assertIn("dispatch_release_convergence", job)
|
|
self.assertIn("release_readiness is the canonical DAG join", job)
|
|
# Runs made without the echo-only join prove its publication jobs.
|
|
self.assertIn("immutable_join=publish_release_tag", job)
|
|
self.assertIn("for reusable_job in publish_docker publish_helm_chart", job)
|
|
self.assertNotIn("docker_build", job)
|
|
self.assertNotIn("helm_smoke", job)
|
|
self.assertIn("failure outside the recoverable activation boundary", job)
|
|
self.assertIn("release-candidate-manifest-${source_sha}-${version}", job)
|
|
self.assertIn("scripts/release_candidate_manifest.py verify-release", job)
|
|
self.assertIn('--release-body-file "${release_body}"', job)
|
|
self.assertLess(
|
|
job.index("--jq '.body // \"\"' > \"${release_body}\""),
|
|
job.index("scripts/release_candidate_manifest.py verify-release"),
|
|
)
|
|
self.assertIn('(any(.assets[]?; .name == "release-activation.json") | not)', job)
|
|
self.assertIn("release-convergence.yml/dispatches", job)
|
|
self.assertIn("return_run_details: true", job)
|
|
self.assertIn("activation_recovery_run_id", job)
|
|
self.assertIn("for attempt in $(seq 1 12)", job)
|
|
self.assertIn("waiting for GitHub indexing", job)
|
|
self.assertIn('--repo "${GITHUB_REPOSITORY}"', job)
|
|
self.assertNotIn("build-release-candidate.yml", recovery)
|
|
self.assertNotIn("scripts/build-release.sh", recovery)
|
|
self.assertIn("Recovered draft activation marker digest does not match", job)
|
|
self.assertIn(".immutable // false", job)
|
|
self.assertIn("verify-github-release-integrity.sh", job)
|
|
self.assertIn('"${verified_marker}"', job)
|
|
|
|
convergence = read(".github/workflows/release-convergence.yml")
|
|
helm_pages_caller = workflow_job_block(convergence, "publish_helm_pages")
|
|
self.assertIn("actions: read", helm_pages_caller)
|
|
self.assertIn("contents: write", helm_pages_caller)
|
|
|
|
marker_upload = job.index('gh release upload "${TAG}"')
|
|
publish_patch = job.index(
|
|
'-X PATCH --input "${publish_payload}"', marker_upload
|
|
)
|
|
committed = job.index("committed=true", publish_patch)
|
|
readback = job.index("curl -fsSL --retry 12", committed)
|
|
integrity = job.index("verify-github-release-integrity.sh", readback)
|
|
self.assertLess(marker_upload, publish_patch)
|
|
self.assertLess(publish_patch, committed)
|
|
self.assertLess(committed, readback)
|
|
self.assertLess(readback, integrity)
|
|
|
|
helm_pages = read(".github/workflows/helm-pages.yml")
|
|
self.assertIn("Require activated GitHub release", helm_pages)
|
|
self.assertIn("release-activation.json", helm_pages)
|
|
self.assertIn("source_release_run_id", helm_pages)
|
|
self.assertIn("Helm Pages refuses to advertise inactive", helm_pages)
|
|
for command in ("view", "upload", "create", "edit"):
|
|
with self.subTest(command=command):
|
|
self.assertRegex(
|
|
helm_pages,
|
|
rf'gh release {command} .*?(?:\\\n\s*)?--repo "\$\{{GITHUB_REPOSITORY\}}"',
|
|
)
|
|
|
|
floating_tags = read(".github/workflows/promote-floating-tags.yml")
|
|
self.assertIn("Require activated GitHub release", floating_tags)
|
|
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")
|
|
retry = read(".github/workflows/retry-release-convergence.yml")
|
|
reconciler = read(
|
|
"scripts/release_control/reconcile_release_convergence.py"
|
|
)
|
|
commit_verdict = workflow_job_block(release_workflow, "release_commit_verdict")
|
|
convergence_verdict = workflow_job_block(convergence, "convergence_verdict")
|
|
|
|
surfaces = {
|
|
"promote_floating_tags": ("FLOATING_RESULT", "Docker aliases"),
|
|
"publish_helm_pages": ("HELM_PAGES_RESULT", "Helm Pages"),
|
|
"promote_private_pro_runtime": ("PRIVATE_PRO_RESULT", "paid-runtime broker"),
|
|
"update_stable_demo": ("DEMO_RESULT", "stable demo"),
|
|
}
|
|
for failed_job, (result_variable, surface_name) in surfaces.items():
|
|
with self.subTest(failed_job=failed_job):
|
|
injected_results = {job: "success" for job in surfaces}
|
|
injected_results[failed_job] = "failure"
|
|
release_state = "committed"
|
|
convergence_state = (
|
|
"debt"
|
|
if any(result != "success" for result in injected_results.values())
|
|
else "converged"
|
|
)
|
|
self.assertEqual(release_state, "committed")
|
|
self.assertEqual(convergence_state, "debt")
|
|
self.assertNotIn(result_variable, commit_verdict)
|
|
self.assertIn(result_variable, convergence_verdict)
|
|
self.assertIn(surface_name, convergence_verdict)
|
|
self.assertIn("the committed release remains public", convergence_verdict)
|
|
|
|
self.assertIn("github.event.workflow_run.conclusion != 'success'", retry)
|
|
self.assertIn("cron: '37 * * * *'", retry)
|
|
self.assertIn("workflow_dispatch:", retry)
|
|
self.assertIn("reconcile_release_convergence.py", retry)
|
|
self.assertIn('--run-id "${RUN_ID}"', retry)
|
|
self.assertIn("--latest", retry)
|
|
self.assertEqual(2, retry.count("PRO_REPOSITORY_TOKEN: ${{ secrets.WORKFLOW_PAT }}"))
|
|
self.assertNotIn("rerun-failed-jobs", retry)
|
|
self.assertIn('actions/runs/{run_id}/rerun', reconciler)
|
|
self.assertIn('release-convergence.yml/dispatches', reconciler)
|
|
self.assertIn("attempts >= max_attempts", reconciler)
|
|
self.assertIn("validate_marker(", reconciler)
|
|
|
|
canonical = read(".github/workflows/canonical-governance.yml")
|
|
self.assertIn(
|
|
"python3 scripts/release_control/reconcile_release_convergence_test.py",
|
|
canonical,
|
|
)
|
|
precommit = read(".husky/pre-commit")
|
|
self.assertIn(
|
|
"show :scripts/release_control/reconcile_release_convergence_test.py",
|
|
precommit,
|
|
)
|
|
|
|
def test_mutating_reusable_workflows_have_no_direct_dispatch_lock_bypass(self) -> None:
|
|
convergence = read(".github/workflows/release-convergence.yml")
|
|
for workflow_path, convergence_job in (
|
|
(".github/workflows/promote-floating-tags.yml", "promote_floating_tags"),
|
|
(".github/workflows/helm-pages.yml", "publish_helm_pages"),
|
|
(".github/workflows/update-demo-server.yml", "update_stable_demo"),
|
|
):
|
|
with self.subTest(workflow_path=workflow_path):
|
|
workflow = read(workflow_path)
|
|
self.assertIn("workflow_call:", workflow)
|
|
self.assertNotIn("workflow_dispatch:", workflow)
|
|
self.assertIn(
|
|
"needs: acquire_customer_promotion_lease",
|
|
workflow_job_block(convergence, convergence_job),
|
|
)
|
|
|
|
dry_run = read(".github/workflows/release-dry-run.yml")
|
|
demo_verification = workflow_job_block(dry_run, "demo_path_preflight")
|
|
self.assertIn("uses: ./.github/workflows/update-demo-server.yml", demo_verification)
|
|
self.assertIn("verify_only: true", demo_verification)
|
|
|
|
retired_bootstrap = read(".github/workflows/deploy-demo-server.yml")
|
|
self.assertIn("Verify Current Committed Stable Demo (No Mutation)", retired_bootstrap)
|
|
self.assertIn("tag: latest", retired_bootstrap)
|
|
self.assertIn("verify_only: true", retired_bootstrap)
|
|
for forbidden in ("go build", "scp ", "docker compose", "verify_only: false"):
|
|
self.assertNotIn(forbidden, retired_bootstrap)
|
|
|
|
# Only the lease owner may call a mutator. The two demo callers outside
|
|
# convergence are verification-only and cannot write the stable host.
|
|
expected_callers = {
|
|
"promote-floating-tags.yml": {"release-convergence.yml"},
|
|
"helm-pages.yml": {"release-convergence.yml"},
|
|
"update-demo-server.yml": {
|
|
"release-convergence.yml",
|
|
"release-dry-run.yml",
|
|
"deploy-demo-server.yml",
|
|
},
|
|
}
|
|
workflow_dir = REPO_ROOT / ".github" / "workflows"
|
|
for callee, expected in expected_callers.items():
|
|
callers = {
|
|
path.name
|
|
for path in workflow_dir.glob("*.yml")
|
|
if f"uses: ./.github/workflows/{callee}" in read(str(path.relative_to(REPO_ROOT)))
|
|
}
|
|
self.assertEqual(callers, expected)
|
|
|
|
def test_live_demo_and_paid_broker_require_exact_committed_lease_owner(self) -> None:
|
|
convergence = read(".github/workflows/release-convergence.yml")
|
|
demo_workflow = read(".github/workflows/update-demo-server.yml")
|
|
demo_mutation = workflow_job_block(demo_workflow, "update-demo")
|
|
demo_caller = workflow_job_block(convergence, "update_stable_demo")
|
|
paid_caller = workflow_job_block(convergence, "promote_private_pro_runtime")
|
|
paid_dispatch = read(".github/workflows/promote-private-pro-runtime.yml")
|
|
|
|
for needle in (
|
|
"Require exact committed activation marker for mutation",
|
|
"inputs.verify_only != true",
|
|
"release-activation.json",
|
|
".convergence_run_id == $convergence_run_id",
|
|
"Stable demo mutation refuses mutable, inactive, or prerelease tag",
|
|
"https://raw.githubusercontent.com/${{ github.repository }}/${LEASE_SHA}/${OWNER_ASSET_NAME}",
|
|
".immutable // false",
|
|
".schema_version == 2",
|
|
):
|
|
self.assertIn(needle, demo_mutation)
|
|
self.assertIn("tag: ${{ inputs.tag }}", demo_caller)
|
|
self.assertIn("verify_only: false", demo_caller)
|
|
self.assertIn("activation_convergence_run_id: ${{ github.run_id }}", demo_caller)
|
|
self.assertIn("convergence_owner_asset_name:", demo_caller)
|
|
self.assertIn("convergence_owner_asset_sha256:", demo_caller)
|
|
|
|
self.assertIn(
|
|
"pulse_lease_sha: ${{ needs.acquire_customer_promotion_lease.outputs.lock_sha }}",
|
|
paid_caller,
|
|
)
|
|
self.assertIn("pulse_convergence_run_id: ${{ github.run_id }}", paid_caller)
|
|
self.assertIn("pulse_owner_asset_name:", paid_caller)
|
|
self.assertIn("pulse_owner_asset_sha256:", paid_caller)
|
|
for needle in (
|
|
"pulse_lease_sha: $pulse_lease_sha",
|
|
"pulse_convergence_run_id: $pulse_convergence_run_id",
|
|
"pulse_owner_asset_name: $pulse_owner_asset_name",
|
|
"pulse_owner_asset_sha256: $pulse_owner_asset_sha256",
|
|
"return_run_details: true",
|
|
".immutable == true",
|
|
"https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${PULSE_LEASE_SHA}/${PULSE_OWNER_ASSET_NAME}",
|
|
".schema_version == 2",
|
|
"Paid-runtime mutation is bound to immutable",
|
|
):
|
|
self.assertIn(needle, paid_dispatch)
|
|
|
|
def test_global_promotion_lease_serializes_and_rejects_out_of_order_channels(self) -> None:
|
|
release_workflow = read(".github/workflows/create-release.yml")
|
|
convergence = read(".github/workflows/release-convergence.yml")
|
|
lease = workflow_job_block(convergence, "acquire_customer_promotion_lease")
|
|
release_lease = workflow_job_block(convergence, "release_customer_promotion_lease")
|
|
lease_script = read("scripts/release_control/customer_promotion_lease.sh")
|
|
|
|
self.assertIn(
|
|
"release-v${{ github.event.inputs.version || github.ref || github.run_id }}",
|
|
release_workflow,
|
|
)
|
|
self.assertNotIn("release-customer-promotion-lock", release_workflow)
|
|
self.assertIn("customer_promotion_lease.sh acquire", lease)
|
|
self.assertIn("refs/heads/release-customer-promotion-lock", lease_script)
|
|
self.assertIn("git push --atomic origin", lease_script)
|
|
self.assertIn('"${lock_commit}:${owner_ref}"', lease_script)
|
|
self.assertIn('"repos/${GITHUB_REPOSITORY}/git/refs"', lease_script)
|
|
self.assertIn("Bootstrapped absent customer-promotion lease ref", lease_script)
|
|
self.assertIn("--force-with-lease=\"${LOCK_REF}:${observed_sha}\"", lease_script)
|
|
self.assertIn("owner_status", lease_script)
|
|
self.assertIn("sort -Vr", lease)
|
|
self.assertIn(".isPrerelease == $prerelease", lease)
|
|
self.assertIn("release-activation.json", lease)
|
|
self.assertIn("superseded=true", lease)
|
|
self.assertIn("no global customer pointer will move backward", lease)
|
|
self.assertIn("customer_promotion_lease.sh release", release_lease)
|
|
self.assertIn("--force-with-lease=\"${LOCK_REF}:${lock_sha}\"", lease_script)
|
|
self.assertIn('schema_version: 2', lease_script)
|
|
self.assertIn('git hash-object -w "${owner_record}"', lease_script)
|
|
self.assertIn('GIT_INDEX_FILE="${lock_index}" git update-index', lease_script)
|
|
self.assertNotIn('gh release upload "${TAG}" "${owner_record}"', lease)
|
|
|
|
helm_pages = workflow_job_block(convergence, "publish_helm_pages")
|
|
convergence_verdict = workflow_job_block(convergence, "convergence_verdict")
|
|
self.assertNotIn("superseded != 'true'", helm_pages)
|
|
self.assertLess(
|
|
convergence_verdict.index('require_success "Helm Pages"'),
|
|
convergence_verdict.index('if [ "${SUPERSEDED}" = "true" ]'),
|
|
)
|
|
for rollback_prone_job in (
|
|
"promote_floating_tags",
|
|
"promote_private_pro_runtime",
|
|
"update_stable_demo",
|
|
):
|
|
self.assertIn(
|
|
"superseded != 'true'",
|
|
workflow_job_block(convergence, rollback_prone_job),
|
|
)
|
|
self.assertIn("without moving rollback-prone pointers", convergence_verdict)
|
|
|
|
def version_key(tag: str) -> tuple[int, int, int, int, int]:
|
|
match = re.fullmatch(r"v(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?", tag)
|
|
self.assertIsNotNone(match)
|
|
assert match is not None
|
|
major, minor, patch, rc = match.groups()
|
|
return (
|
|
int(major),
|
|
int(minor),
|
|
int(patch),
|
|
1 if rc is None else 0,
|
|
int(rc or 0),
|
|
)
|
|
|
|
def admitted(target: str, committed_same_channel: tuple[str, ...]) -> bool:
|
|
return target == max(committed_same_channel, key=version_key)
|
|
|
|
self.assertFalse(admitted("v6.2.0", ("v6.2.0", "v6.2.1")))
|
|
self.assertTrue(admitted("v6.2.1", ("v6.2.0", "v6.2.1")))
|
|
self.assertFalse(admitted("v6.3.0-rc.1", ("v6.3.0-rc.1", "v6.3.0-rc.2")))
|
|
self.assertTrue(admitted("v6.3.0-rc.2", ("v6.3.0-rc.1", "v6.3.0-rc.2")))
|
|
self.assertTrue(admitted("v6.2.1", ("v6.2.1",)))
|
|
self.assertTrue(admitted("v6.3.0-rc.2", ("v6.3.0-rc.2",)))
|
|
|
|
helm_index = {"v6.2.1"}
|
|
pointer_target = "v6.2.1"
|
|
older = "v6.2.0"
|
|
helm_index.add(older)
|
|
if admitted(older, (older, pointer_target)):
|
|
pointer_target = older
|
|
self.assertEqual(helm_index, {"v6.2.0", "v6.2.1"})
|
|
self.assertEqual(pointer_target, "v6.2.1")
|
|
|
|
rc_helm_index = {"v6.3.0-rc.2"}
|
|
rc_pointer_target = "v6.3.0-rc.2"
|
|
older_rc = "v6.3.0-rc.1"
|
|
rc_helm_index.add(older_rc)
|
|
if admitted(older_rc, (older_rc, rc_pointer_target)):
|
|
rc_pointer_target = older_rc
|
|
self.assertEqual(rc_helm_index, {"v6.3.0-rc.1", "v6.3.0-rc.2"})
|
|
self.assertEqual(rc_pointer_target, "v6.3.0-rc.2")
|
|
|
|
def test_prepare_to_commit_handoff_survives_delayed_activation(self) -> None:
|
|
release_workflow = read(".github/workflows/create-release.yml")
|
|
convergence = read(".github/workflows/release-convergence.yml")
|
|
retry = read(".github/workflows/retry-release-convergence.yml")
|
|
reconciler = read(
|
|
"scripts/release_control/reconcile_release_convergence.py"
|
|
)
|
|
activation = workflow_job_block(release_workflow, "activate_release")
|
|
|
|
self.assertIn(
|
|
"Release convergence ${{ inputs.tag }} source ${{ inputs.source_release_run_id }}",
|
|
convergence,
|
|
)
|
|
await_commit = workflow_job_block(convergence, "await_activation_commit")
|
|
self.assertIn("timeout-minutes: 360", await_commit)
|
|
self.assertIn('max_attempts=10500', await_commit)
|
|
self.assertIn('sleep 2', await_commit)
|
|
self.assertIn("source_release_run_id", await_commit)
|
|
self.assertIn('gh run view "${EXPECTED_SOURCE_RUN_ID}"', await_commit)
|
|
self.assertIn('source_status}" = "completed"', await_commit)
|
|
self.assertIn("completed without the exact activation marker", await_commit)
|
|
self.assertIn('source_conclusion}" != "success"', await_commit)
|
|
self.assertIn('select(.name == "release-activation.json") | .state', await_commit)
|
|
self.assertIn('marker_asset_state}" != "uploaded"', await_commit)
|
|
self.assertIn("max_completed_propagation_attempts=20", await_commit)
|
|
self.assertIn("uploaded but not publicly readable yet", await_commit)
|
|
self.assertIn("does not match the expected immutable release identity", await_commit)
|
|
self.assertIn("EXPECTED_CONVERGENCE_RUN_ID: ${{ github.run_id }}", await_commit)
|
|
self.assertIn('original_status}" != "completed"', await_commit)
|
|
self.assertIn("successor adoption is not yet allowed", await_commit)
|
|
self.assertIn("activation_marker_sha256", await_commit)
|
|
|
|
self.assertIn("--run-id", retry)
|
|
self.assertIn("DISPLAY_TITLE = re.compile", reconciler)
|
|
self.assertIn("source_run_id = int(source_text)", reconciler)
|
|
self.assertIn('actions/runs/{source_run_id}', reconciler)
|
|
self.assertIn('source.get("status") != "completed"', reconciler)
|
|
self.assertIn('< 50', reconciler)
|
|
self.assertIn("Renewed pre-commit convergence owner", reconciler)
|
|
|
|
self.assertIn("require_viable_convergence_owner()", activation)
|
|
self.assertEqual(activation.count("require_viable_convergence_owner"), 3)
|
|
self.assertIn('gh run view "${CONVERGENCE_RUN_ID}"', activation)
|
|
self.assertIn("for attempt in $(seq 1 12)", activation)
|
|
self.assertIn("waiting for GitHub indexing", activation)
|
|
self.assertIn("--json event,status,conclusion,workflowName,displayTitle,url", activation)
|
|
self.assertIn('expected_title="Release convergence ${TAG} source ${GITHUB_RUN_ID}"', activation)
|
|
self.assertIn('[ "${owner_status}" = "completed" ]', activation)
|
|
self.assertIn("before staging the exact marker", activation)
|
|
self.assertIn("validate_existing_activation_commit", activation)
|
|
self.assertIn(
|
|
"Recover release activation ${TAG} source ${GITHUB_RUN_ID}", activation
|
|
)
|
|
self.assertIn(
|
|
'.path == ".github/workflows/recover-release-activation.yml"',
|
|
activation,
|
|
)
|
|
self.assertIn(
|
|
'.path == ".github/workflows/release-convergence.yml"', activation
|
|
)
|
|
verdict = workflow_job_block(release_workflow, "release_commit_verdict")
|
|
self.assertIn("activation_recovery_run_id", verdict)
|
|
self.assertIn(
|
|
'.status == "completed" and .conclusion == "success"', verdict
|
|
)
|
|
self.assertLess(
|
|
activation.index(
|
|
"require_viable_convergence_owner\n"
|
|
" if [ -z \"${IMMUTABILITY_ADMIN_TOKEN:-}\" ]"
|
|
),
|
|
activation.index("-X PATCH --input \"$publish_payload\""),
|
|
)
|
|
marker_upload = activation.index('gh release upload "${TAG}"')
|
|
final_owner_check = activation.rindex("require_viable_convergence_owner")
|
|
publish_patch = activation.index(
|
|
'-X PATCH --input "$publish_payload"', final_owner_check
|
|
)
|
|
self.assertLess(marker_upload, final_owner_check)
|
|
self.assertLess(final_owner_check, publish_patch)
|
|
|
|
def test_fresh_fixed_code_convergence_can_adopt_completed_original_owner(self) -> None:
|
|
convergence = read(".github/workflows/release-convergence.yml")
|
|
await_commit = workflow_job_block(convergence, "await_activation_commit")
|
|
lease = workflow_job_block(convergence, "acquire_customer_promotion_lease")
|
|
lease_script = read("scripts/release_control/customer_promotion_lease.sh")
|
|
|
|
original_owner = "100"
|
|
successor_owner = "200"
|
|
original_status = "completed"
|
|
marker_lineage = {
|
|
"tag": "v6.2.0",
|
|
"source_release_run_id": "50",
|
|
"convergence_run_id": original_owner,
|
|
"r2_prefix": "v6.2.0-pro-20260808-50",
|
|
}
|
|
successor_may_adopt = (
|
|
marker_lineage["convergence_run_id"] != successor_owner
|
|
and original_status == "completed"
|
|
)
|
|
self.assertTrue(successor_may_adopt)
|
|
self.assertIn("Adopting committed ${TAG} from completed convergence owner", await_commit)
|
|
self.assertIn("ACTIVATION_OWNER_RUN_ID", lease)
|
|
self.assertIn("ACTIVATION_MARKER_SHA256", lease)
|
|
self.assertIn(
|
|
"release-convergence-owner-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.json",
|
|
lease_script,
|
|
)
|
|
self.assertIn("owner_asset_sha256", lease_script)
|
|
self.assertIn("git commit-tree", lease_script)
|
|
self.assertNotIn("gh release upload", lease)
|
|
|
|
stale_name = f"release-convergence-owner-{original_owner}-5.json"
|
|
successor_name = f"release-convergence-owner-{successor_owner}-1.json"
|
|
self.assertNotEqual(stale_name, successor_name)
|
|
|
|
def test_customer_promotion_lease_commit_contains_exact_owner_evidence(self) -> None:
|
|
script = REPO_ROOT / "scripts" / "release_control" / "customer_promotion_lease.sh"
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
remote = root / "remote.git"
|
|
checkout = root / "checkout"
|
|
output = root / "github-output"
|
|
git_subprocess_env = strip_local_git_env(os.environ.copy())
|
|
subprocess.run(
|
|
["git", "init", "--bare", str(remote)],
|
|
check=True,
|
|
capture_output=True,
|
|
env=git_subprocess_env,
|
|
)
|
|
subprocess.run(
|
|
["git", "init", str(checkout)],
|
|
check=True,
|
|
capture_output=True,
|
|
env=git_subprocess_env,
|
|
)
|
|
for key, value in (
|
|
("user.name", "Pulse Test"),
|
|
("user.email", "pulse-test@example.invalid"),
|
|
):
|
|
subprocess.run(
|
|
["git", "config", key, value],
|
|
cwd=checkout,
|
|
check=True,
|
|
env=git_subprocess_env,
|
|
)
|
|
(checkout / "README").write_text("base\n", encoding="utf-8")
|
|
subprocess.run(
|
|
["git", "add", "README"],
|
|
cwd=checkout,
|
|
check=True,
|
|
env=git_subprocess_env,
|
|
)
|
|
subprocess.run(
|
|
["git", "commit", "-m", "base"],
|
|
cwd=checkout,
|
|
check=True,
|
|
capture_output=True,
|
|
env=git_subprocess_env,
|
|
)
|
|
subprocess.run(
|
|
["git", "remote", "add", "origin", str(remote)],
|
|
cwd=checkout,
|
|
check=True,
|
|
env=git_subprocess_env,
|
|
)
|
|
subprocess.run(
|
|
["git", "push", "origin", "HEAD:refs/heads/release-customer-promotion-lock"],
|
|
cwd=checkout,
|
|
check=True,
|
|
capture_output=True,
|
|
env=git_subprocess_env,
|
|
)
|
|
|
|
env = git_subprocess_env.copy()
|
|
env.update(
|
|
{
|
|
"GH_TOKEN": "test-token",
|
|
"GITHUB_REPOSITORY": "rcourtman/Pulse",
|
|
"GITHUB_RUN_ID": "200",
|
|
"GITHUB_RUN_ATTEMPT": "3",
|
|
"GITHUB_ACTOR": "pulse-test",
|
|
"GITHUB_ACTOR_ID": "1234",
|
|
"GITHUB_OUTPUT": str(output),
|
|
"TARGET_COMMITISH": "a" * 40,
|
|
"RELEASE_ID": "321",
|
|
"SOURCE_RELEASE_RUN_ID": "100",
|
|
"R2_PREFIX": "v6.5.0-pro-test",
|
|
"ACTIVATION_OWNER_RUN_ID": "150",
|
|
"ACTIVATION_MARKER_SHA256": "b" * 64,
|
|
}
|
|
)
|
|
result = subprocess.run(
|
|
[str(script), "acquire", "v6.5.0"],
|
|
cwd=checkout,
|
|
env=env,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
outputs = dict(
|
|
line.split("=", 1)
|
|
for line in output.read_text(encoding="utf-8").splitlines()
|
|
)
|
|
self.assertRegex(outputs["lock_sha"], r"^[0-9a-f]{40}$")
|
|
self.assertEqual(
|
|
outputs["owner_asset_name"],
|
|
"release-convergence-owner-200-3.json",
|
|
)
|
|
record_text = subprocess.run(
|
|
[
|
|
"git",
|
|
"show",
|
|
f"{outputs['lock_sha']}:{outputs['owner_asset_name']}",
|
|
],
|
|
cwd=checkout,
|
|
text=True,
|
|
capture_output=True,
|
|
check=True,
|
|
).stdout
|
|
record = json.loads(record_text)
|
|
self.assertEqual(record["schema_version"], 2)
|
|
self.assertEqual(record["convergence_run_id"], "200")
|
|
self.assertEqual(record["activation_owner_run_id"], "150")
|
|
self.assertNotIn("lease_sha", record)
|
|
self.assertEqual(
|
|
outputs["owner_asset_sha256"],
|
|
hashlib.sha256(record_text.encode()).hexdigest(),
|
|
)
|
|
retained = subprocess.run(
|
|
[
|
|
"git",
|
|
"ls-remote",
|
|
"origin",
|
|
"refs/tags/release-convergence-owner-200-3",
|
|
],
|
|
cwd=checkout,
|
|
text=True,
|
|
capture_output=True,
|
|
check=True,
|
|
).stdout.split()[0]
|
|
self.assertEqual(retained, outputs["lock_sha"])
|
|
|
|
def test_private_dispatches_wait_for_the_exact_created_run(self) -> None:
|
|
release_workflow = read(".github/workflows/create-release.yml")
|
|
private_promotion_workflow = read(
|
|
".github/workflows/promote-private-pro-runtime.yml"
|
|
)
|
|
cases = (
|
|
(
|
|
release_workflow,
|
|
"stage_private_pro_runtime",
|
|
"repos/rcourtman/pulse-enterprise/actions/workflows/build-pro-release.yml/dispatches",
|
|
"build_run_id",
|
|
),
|
|
(
|
|
private_promotion_workflow,
|
|
"promote",
|
|
"repos/rcourtman/pulse-pro/actions/workflows/promote-paid-runtime-release.yml/dispatches",
|
|
"promote_run_id",
|
|
),
|
|
)
|
|
|
|
for workflow, job_name, endpoint, run_id_variable in cases:
|
|
with self.subTest(job_name=job_name):
|
|
job = workflow_job_block(workflow, job_name)
|
|
self.assertIn("return_run_details: true", job)
|
|
self.assertIn(
|
|
'-H "X-GitHub-Api-Version: 2026-03-10"',
|
|
job,
|
|
)
|
|
self.assertIn(endpoint, job)
|
|
self.assertIn(
|
|
f'{run_id_variable}="$(jq -r \'.workflow_run_id // empty\'',
|
|
job,
|
|
)
|
|
self.assertIn(f'"${{{run_id_variable}}}"', job)
|
|
self.assertIn('local run_id="$2"', job)
|
|
self.assertIn('gh run view "${run_id}"', job)
|
|
self.assertIn("did not return an exact workflow run ID", job)
|
|
self.assertNotIn("gh run list", job)
|
|
self.assertNotIn("started_at", job)
|
|
|
|
def test_release_promotion_policy_requires_live_rc_and_v5_policy(self) -> None:
|
|
content = read("docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md")
|
|
self.assertIn("`beta.N` is the normal user-testing stage", content)
|
|
self.assertIn("`rc.N` is the release train's candidate for stable", content)
|
|
self.assertIn("Stable promotion lineage must come from a published `rc.N`", content)
|
|
self.assertIn("live run of the release pipeline for the prerelease tag itself", content)
|
|
self.assertIn("an accidental git tag by itself", content)
|
|
self.assertIn("does not count as a shipped prerelease", content)
|
|
self.assertIn("do not promote to `stable` until the active control-plane target", content)
|
|
self.assertIn("A live release-pipeline exercise already completed for the promoted prerelease tag", content)
|
|
self.assertIn("maintenance-only window lasts 90 calendar days", content)
|
|
self.assertIn("V5_MAINTENANCE_SUPPORT_POLICY.md", content)
|
|
self.assertIn("release notes may keep placeholder", content)
|
|
self.assertIn("Exact v6 GA and v5 end-of-support dates locked before GA publish", content)
|
|
self.assertIn("governed prerelease and stable release branches", content)
|
|
self.assertIn("Customer-facing private Pulse Pro archives", content)
|
|
self.assertIn("public alpha, beta, or RC tag", content)
|
|
self.assertIn("license.pulserelay.pro/pulse-pro:6.0.0", content)
|
|
self.assertIn("moving branch", content)
|
|
self.assertIn("`implemented`", content)
|
|
self.assertIn("`release-validated`", content)
|
|
self.assertIn("`live-verified`", content)
|
|
self.assertIn("scripts/release_control/live_runtime_proof.py", content)
|
|
self.assertIn("an operator statement cannot substitute for the receipt", content)
|
|
|
|
|
|
def test_live_runtime_claim_rejects_unknown_successful_posture(self) -> None:
|
|
observed, failures = evaluate_live_runtime(
|
|
expected_version="6.2.0-rc.8",
|
|
observed_version="6.2.0-rc.8",
|
|
postures=[
|
|
{
|
|
"subjectResourceId": "vm-100",
|
|
"state": "unknown",
|
|
"lastSuccessfulPointAt": "2026-08-04T10:00:00Z",
|
|
}
|
|
],
|
|
minimum_postures=1,
|
|
minimum_successful_postures=1,
|
|
)
|
|
self.assertEqual(observed["unknownWithSuccessfulPointCount"], 1)
|
|
self.assertTrue(any("remain unknown" in failure for failure in failures))
|
|
|
|
def test_v6_ga_owner_risk_exception_is_bounded_and_packet_aligned(self) -> None:
|
|
policy = read("docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md")
|
|
checklist = read("docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md")
|
|
owner_record = read(
|
|
"docs/release-control/v6/internal/records/current-branch-ga-owner-approval-2026-07-02.md"
|
|
)
|
|
release_notes = read("docs/releases/RELEASE_NOTES_v6.md")
|
|
v5_policy = read("docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md")
|
|
status = read_json("docs/release-control/v6/internal/status.json")
|
|
control_plane = read_json("docs/release-control/control_plane.json")
|
|
|
|
normalized_policy = normalize_ws(policy)
|
|
normalized_checklist = normalize_ws(checklist)
|
|
normalized_owner_record = normalize_ws(owner_record)
|
|
normalized_release_notes = normalize_ws(release_notes)
|
|
normalized_v5_policy = normalize_ws(v5_policy)
|
|
|
|
self.assertIn("v6.0.0 owner-risk exception", policy)
|
|
self.assertIn("bounded v6.0.0 release-owner risk acceptance", normalized_policy)
|
|
self.assertIn("not validation evidence for the post-RC7 changes", normalized_policy)
|
|
self.assertIn("not a standing policy for later stable releases", normalized_policy)
|
|
self.assertIn("Additional RC or soak required: no", normalized_owner_record)
|
|
self.assertIn("Additional current-branch validation required before GA: no", normalized_owner_record)
|
|
self.assertIn("accepting the remaining current-branch validation risk", normalized_owner_record)
|
|
self.assertIn("not validation evidence for the post-RC7", normalized_checklist)
|
|
|
|
self.assertIn("current `pulse/v6-release` branch", normalized_release_notes)
|
|
self.assertIn("seven release candidates and accumulated post-RC7 fixes", normalized_release_notes)
|
|
self.assertIn("Pulse v5 entered maintenance-only support on `2026-07-04`.", normalized_release_notes)
|
|
self.assertIn("existing v5 users until `2026-10-02`.", normalized_release_notes)
|
|
self.assertIn("Pulse v5 entered maintenance-only support on 2026-07-04.", normalized_v5_policy)
|
|
self.assertIn("2026-10-02. After 2026-10-02", normalized_v5_policy)
|
|
|
|
gate = next(gate for gate in status["release_gates"] if gate["id"] == "rc-to-ga-promotion-readiness")
|
|
self.assertEqual(gate["status"], "passed")
|
|
self.assertIn("owner risk acceptance", gate["summary"])
|
|
self.assertIn(
|
|
"docs/release-control/v6/internal/records/current-branch-ga-owner-approval-2026-07-02.md",
|
|
{item["path"] for item in gate["evidence"]},
|
|
)
|
|
|
|
decisions = {decision["id"]: decision for decision in status["resolved_decisions"]}
|
|
self.assertEqual(
|
|
decisions["current-branch-ga-owner-risk-acceptance"]["decided_at"],
|
|
"2026-07-02",
|
|
)
|
|
|
|
ga_target = next(target for target in control_plane["targets"] if target["id"] == "v6-ga-promotion")
|
|
self.assertIn("Pulse v6 GA", ga_target["summary"])
|
|
self.assertIn("main is the canonical latest-and-greatest branch", ga_target["summary"])
|
|
|
|
def test_pre_release_checklist_tracks_rc_to_ga_gate_inputs(self) -> None:
|
|
content = read("docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md")
|
|
self.assertIn("release pipeline has already been exercised on a real prerelease tag", content)
|
|
self.assertIn("not an accidental git tag", content)
|
|
self.assertIn("V5_MAINTENANCE_SUPPORT_POLICY.md", content)
|
|
self.assertIn("replace any placeholder GA notice dates", content)
|
|
self.assertIn("rc-to-ga-rehearsal-summary", content)
|
|
self.assertIn("rc-to-ga-promotion-readiness", content)
|
|
self.assertIn("record_rc_to_ga_rehearsal.py --run-id <run-id>", content)
|
|
self.assertIn("rc-to-ga-promotion-readiness-rehearsal-<record-date>.md", content)
|
|
self.assertIn(promotion_metadata_envelope(), normalize_ws(content))
|
|
|
|
def test_v5_support_policy_and_release_notes_publish_exact_notice(self) -> None:
|
|
policy = read("docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md")
|
|
release_notes = read("docs/releases/RELEASE_NOTES_v6.md")
|
|
self.assertIn("maintenance-only support immediately on the v6 GA date", policy)
|
|
self.assertIn("90 calendar days from the v6 GA", policy)
|
|
self.assertIn("pulse/v5-maintenance", policy)
|
|
if "Pulse v5 Support Transition" in release_notes:
|
|
self.assertIn("publish an explicit exception", release_notes)
|
|
self.assertRegex(
|
|
release_notes,
|
|
re.compile(r"Pulse v5 entered maintenance-only support on `(?:\[v6-ga-date\]|\d{4}-\d{2}-\d{2})`\.")
|
|
)
|
|
self.assertRegex(
|
|
release_notes,
|
|
re.compile(r"existing v5 users until `(?:\[v5-eos-date\]|\d{4}-\d{2}-\d{2})`\.")
|
|
)
|
|
else:
|
|
self.assertRegex(release_notes, re.compile(r"(Pre-Release Notes|Release Candidate Notes)"))
|
|
self.assertRegex(release_notes, re.compile(r"(final GA release|stable `v6\.0\.0` release)"))
|
|
self.assertNotIn("Pulse v5 Support Transition", release_notes)
|
|
|
|
def test_release_notes_index_points_at_current_rc_packet(self) -> None:
|
|
release_index = read("docs/RELEASE_NOTES.md")
|
|
# Stable + shipped-RC1 packet paths are hardcoded because they don't
|
|
# follow the *_DRAFT.md naming pattern that distinguishes in-flight
|
|
# prerelease packets.
|
|
for path in (
|
|
"docs/releases/RELEASE_NOTES_v6.md",
|
|
"docs/releases/V6_CHANGELOG.md",
|
|
"docs/UPGRADE_v6.md",
|
|
"docs/releases/RELEASE_NOTES_v6_RC1.md",
|
|
"docs/releases/V6_CHANGELOG_RC1.md",
|
|
"docs/releases/V6_RC_OPERATOR_SUPPORT_PACK.md",
|
|
):
|
|
self.assertIn(path, release_index)
|
|
# Every discovered RC draft packet (rc.2 onward) must be linked.
|
|
for _, release_notes, changelog, support_pack in discover_rc_draft_packets():
|
|
with self.subTest(packet=release_notes):
|
|
self.assertIn(release_notes, release_index)
|
|
self.assertIn(changelog, release_index)
|
|
self.assertIn(support_pack, release_index)
|
|
|
|
def test_operator_support_packs_keep_free_first_paid_continuity_wording(self) -> None:
|
|
support_pack_paths = ("docs/releases/V6_RC_OPERATOR_SUPPORT_PACK.md",) + tuple(
|
|
support_pack for _, _, _, support_pack in discover_rc_draft_packets()
|
|
)
|
|
for rel in support_pack_paths:
|
|
with self.subTest(rel=rel):
|
|
support_pack = read(rel)
|
|
self.assertIn(
|
|
"keep the current recurring price, with self-hosted monitoring and",
|
|
support_pack,
|
|
)
|
|
self.assertIn("child-resource volume not metered", support_pack)
|
|
self.assertIn("core monitoring included", support_pack)
|
|
self.assertNotIn("uncapped monitored-system plus guest", support_pack)
|
|
self.assertNotIn("uncapped monitored-system and guest capacity", support_pack)
|
|
self.assertNotIn("guest-capacity continuity", support_pack)
|
|
self.assertNotIn("core monitoring unlimited", normalize_ws(support_pack))
|
|
|
|
def test_stable_release_packet_describes_platform_shaped_frontend_on_unified_backend(self) -> None:
|
|
"""After the rc.6 IA revert, the stable v6 release docs must describe
|
|
the frontend as platform-shaped (Proxmox, Docker, Kubernetes, TrueNAS,
|
|
vSphere, Standalone) on a unified resource backend. Drift back to the
|
|
rc.1-rc.5 unified Infrastructure/Workloads/Storage/Recovery framing in
|
|
the stable packet would mislead operators reading the canonical v6
|
|
release notes.
|
|
"""
|
|
stable_docs = {
|
|
"release_notes": read("docs/releases/RELEASE_NOTES_v6.md"),
|
|
"changelog": read("docs/releases/V6_CHANGELOG.md"),
|
|
"operator_support_pack": read("docs/releases/V6_RC_OPERATOR_SUPPORT_PACK.md"),
|
|
}
|
|
|
|
for name, content in stable_docs.items():
|
|
with self.subTest(name=name):
|
|
normalized = normalize_ws(content)
|
|
# Anti-patterns for prior product shapes the stable packet
|
|
# must not describe as current truth.
|
|
self.assertNotIn("default route lands on `Dashboard`", normalized)
|
|
self.assertNotIn("around Dashboard, Infrastructure", normalized)
|
|
self.assertNotIn(
|
|
"`Dashboard`, `Infrastructure`, `Workloads`, `Storage`, and `Recovery`",
|
|
content,
|
|
)
|
|
self.assertNotIn("- `Dashboard`", content)
|
|
# Anti-patterns for the reverted unified IA so the stable
|
|
# packet does not silently drift back to it.
|
|
self.assertNotIn(
|
|
"Authenticated users now land on `Infrastructure`",
|
|
normalized,
|
|
)
|
|
self.assertNotIn(
|
|
"default route lands on `Infrastructure`",
|
|
normalized,
|
|
)
|
|
self.assertNotIn(
|
|
"Infrastructure as the default landing surface",
|
|
content,
|
|
)
|
|
|
|
self.assertIn(
|
|
"platform-shaped top-level navigation",
|
|
normalize_ws(stable_docs["release_notes"]),
|
|
)
|
|
self.assertIn(
|
|
"platform-shaped",
|
|
normalize_ws(stable_docs["changelog"]),
|
|
)
|
|
self.assertIn(
|
|
"platform-shaped top-level pages",
|
|
normalize_ws(stable_docs["operator_support_pack"]),
|
|
)
|
|
|
|
def test_rc1_changelog_keeps_current_free_first_licensing_posture(self) -> None:
|
|
changelog = read("docs/releases/V6_CHANGELOG_RC1.md")
|
|
normalized = normalize_ws(changelog)
|
|
self.assertIn("Pricing/limit note", changelog)
|
|
self.assertIn("include core monitoring by default", normalized)
|
|
self.assertIn("not a monitoring-volume paywall", normalized)
|
|
self.assertNotIn(
|
|
"monitored-system limits, commercial posture, and trial eligibility",
|
|
changelog,
|
|
)
|
|
self.assertNotIn("Limits are applied to canonical top-level monitored systems", changelog)
|
|
|
|
def test_release_notes_title_uses_exact_first_heading(self) -> None:
|
|
for version in ("6.4.5", "6.5.0", "6.5.0-rc.1"):
|
|
for suffix in ("", " Release Notes"):
|
|
with self.subTest(version=version, suffix=suffix):
|
|
self.assertTrue(release_notes_have_exact_title(
|
|
f"\n# Pulse v{version}{suffix}\n\nUser-facing changes.\n", version))
|
|
for title in (
|
|
"# Pulse v6.5.1", "# Pulse v6.5.0-rc.1", "# Pulse v6.5.00",
|
|
"# Pulse v6.5.0-beta.1", "## Pulse v6.5.0", "# Other Pulse v6.5.0",
|
|
"# Pulse v6x5x0", "# Pulse v6.5.0 Release Notes extra",
|
|
"# Pulse v6.5.0extra", "User-facing changes before the title.",
|
|
):
|
|
with self.subTest(title=title):
|
|
self.assertFalse(release_notes_have_exact_title(
|
|
title + "\n\n# Pulse v6.5.0 Release Notes\n\n`v6.5.0`\n", "6.5.0"))
|
|
self.assertFalse(release_notes_have_exact_title("\n \n", "6.5.0"))
|
|
authored = read("scripts/installtests/testdata/release-notes-v6.4.5-authored.md")
|
|
self.assertTrue(release_notes_have_exact_title(authored, "6.4.5"))
|
|
|
|
def test_version_file_matches_current_rc_packet(self) -> None:
|
|
current_version = read("VERSION").strip()
|
|
release_index = read("docs/RELEASE_NOTES.md")
|
|
stable_packet_paths = stable_packet_paths_for_version(current_version)
|
|
if stable_packet_paths is not None:
|
|
release_notes_path, changelog_path = stable_packet_paths
|
|
release_notes = read(release_notes_path)
|
|
changelog = read(changelog_path)
|
|
self.assertIn(release_notes_path, release_index)
|
|
self.assertIn(changelog_path, release_index)
|
|
self.assertTrue(release_notes_have_exact_title(release_notes, current_version))
|
|
self.assertIn(f"Pulse v{current_version}", changelog)
|
|
else:
|
|
packet_paths = rc_packet_paths_for_version(current_version)
|
|
support_packet_paths = support_prerelease_packet_paths_for_version(current_version)
|
|
self.assertTrue(
|
|
packet_paths is not None or support_packet_paths is not None,
|
|
f"VERSION={current_version} does not match a governed v6 prerelease packet pattern",
|
|
)
|
|
if packet_paths is not None:
|
|
release_notes_path, changelog_path, operator_pack_path = packet_paths
|
|
|
|
release_notes = read(release_notes_path)
|
|
changelog = read(changelog_path)
|
|
operator_pack = read(operator_pack_path)
|
|
|
|
self.assertIn(f"current in-repo v6 `rc.{current_version.rsplit('.', 1)[1]}` draft packet", release_index)
|
|
self.assertIn(release_notes_path, release_index)
|
|
self.assertIn(changelog_path, release_index)
|
|
self.assertIn(operator_pack_path, release_index)
|
|
self.assertNotIn("current stable v6 release packet", release_index)
|
|
self.assertIn(f"Pulse v{current_version} Draft Release Notes", release_notes)
|
|
self.assertIn(f"`v{current_version}`", release_notes)
|
|
self.assertIn(f"Pulse v{current_version} Draft Changelog", changelog)
|
|
self.assertIn(f"`v{current_version}`", operator_pack)
|
|
else:
|
|
assert support_packet_paths is not None
|
|
release_notes_path, changelog_path = support_packet_paths
|
|
|
|
release_notes = read(release_notes_path)
|
|
changelog = read(changelog_path)
|
|
|
|
packet_label = "beta" if "-beta." in current_version else "release candidate"
|
|
self.assertIn(f"current v6 {packet_label} packet", release_index)
|
|
self.assertIn(release_notes_path, release_index)
|
|
self.assertIn(changelog_path, release_index)
|
|
self.assertTrue(release_notes_have_exact_title(release_notes, current_version))
|
|
self.assertIn(f"Pulse v{current_version}", changelog)
|
|
|
|
def test_v611_packet_records_proxmox_backup_posture_identity_fix(self) -> None:
|
|
release_notes = normalize_ws(read("docs/releases/RELEASE_NOTES_v6.1.1.md"))
|
|
changelog = normalize_ws(read("docs/releases/V6_CHANGELOG_v6.1.1.md"))
|
|
|
|
self.assertIn(
|
|
"Proxmox backup and snapshot recovery points preserve canonical workload identity",
|
|
release_notes,
|
|
)
|
|
self.assertIn("obsolete protection-posture row is removed", release_notes)
|
|
self.assertIn(
|
|
"Proxmox backup, snapshot, and PBS recovery points preserve canonical workload identity",
|
|
changelog,
|
|
)
|
|
self.assertIn("posture refresh removes obsolete rows", changelog)
|
|
|
|
def test_upgrade_guide_qualifies_pinned_server_updates(self) -> None:
|
|
upgrade_guide = read("docs/UPGRADE_v6.md")
|
|
server_updates = normalize_ws(
|
|
upgrade_guide.split("### systemd and Proxmox LXC installs", 1)[1].split("\n### ", 1)[0]
|
|
)
|
|
self.assertNotRegex(server_updates, r"```(?:bash|sh|shell)\b")
|
|
self.assertIn("INSTALL.md#manual-update", server_updates)
|
|
self.assertIn("helper's ownership", server_updates)
|
|
self.assertIn("Do not execute an absent or unknown helper", server_updates)
|
|
self.assertIn("default installation paths", server_updates)
|
|
self.assertIn("AUTO_UPDATE.md#a-helper-selects-a-helm-chart-release", server_updates)
|
|
self.assertIn("do not replace a private Pro runtime", server_updates)
|
|
self.assertIn("running version, edition and service health", server_updates)
|
|
|
|
def test_manual_update_help_preserves_helper_ownership_and_software_identity(self) -> None:
|
|
automatic_updates = read("docs/AUTO_UPDATE.md")
|
|
lxc_updates = normalize_ws(
|
|
automatic_updates.split("### ProxmoxVE LXC (Manual)", 1)[1].split("\n### ", 1)[0]
|
|
)
|
|
systemd_updates = normalize_ws(
|
|
automatic_updates.split("### Systemd Service (Manual)", 1)[1].split("\n### ", 1)[0]
|
|
)
|
|
chart_help = normalize_ws(
|
|
automatic_updates.split("### A helper selects a Helm-chart release", 1)[1]
|
|
.split("\n### ", 1)[0]
|
|
)
|
|
manual_update = normalize_ws(
|
|
read("docs/INSTALL.md").split("#### Manual Update", 1)[1].split("\n#### ", 1)[0]
|
|
)
|
|
release_guide = normalize_ws(
|
|
read("docs/RELEASE_PROCESS.md").split("## Software releases and Helm charts", 1)[1]
|
|
.split("\n## ", 1)[0]
|
|
)
|
|
|
|
# Newer canonical help delegates to the ownership-checked procedure
|
|
# instead of copying a command into either host entry point. Keep that
|
|
# stronger boundary, including custom paths and private Pro installs.
|
|
for entry in (lxc_updates, systemd_updates):
|
|
self.assertIn("](INSTALL.md#-updates)", entry)
|
|
self.assertNotIn("sudo /bin/update", entry)
|
|
self.assertIn("inside the existing Pulse LXC", lxc_updates)
|
|
self.assertIn("community-scripts installation", lxc_updates)
|
|
self.assertIn("helper whose owner is unknown", systemd_updates)
|
|
self.assertIn("default installation paths", systemd_updates)
|
|
self.assertIn("helper is absent or its owner is unknown", manual_update)
|
|
self.assertIn("stop before executing it", manual_update)
|
|
self.assertIn("existing service and data/config paths", manual_update)
|
|
self.assertIn("private Pro runtime with a public build", manual_update)
|
|
self.assertIn("unknown helper may ignore `--version`", chart_help)
|
|
self.assertIn("exact `PULSE_VERSION`", chart_help)
|
|
self.assertIn("check the running version and service health", chart_help)
|
|
self.assertIn("Private Pro installations must retain their private runtime", chart_help)
|
|
self.assertIn("Do not recreate the LXC", chart_help)
|
|
|
|
self.assertIn("`helm-chart-*`", chart_help)
|
|
self.assertIn("not the Linux server archive", chart_help)
|
|
self.assertIn("List order does not establish the latest software release", release_guide)
|
|
self.assertIn("cannot update a systemd or Proxmox LXC server", release_guide)
|
|
self.assertIn("disabling signature verification", release_guide)
|
|
self.assertIn("Settings → System → Updates", release_guide)
|
|
|
|
def test_upgrade_guide_points_at_current_rc_support_pack(self) -> None:
|
|
upgrade_guide = read("docs/UPGRADE_v6.md")
|
|
current_version = read("VERSION").strip()
|
|
self.assertIn("the historical Pulse update signer was not recovered", normalize_ws(upgrade_guide))
|
|
self.assertIn("explicit verified trust migration", normalize_ws(upgrade_guide))
|
|
self.assertIn("not an instruction to reinstall or bypass signature verification", normalize_ws(upgrade_guide))
|
|
self.assertIn("historical choice is not a safe downgrade procedure", normalize_ws(upgrade_guide))
|
|
self.assertIn("### License and Entitlements", upgrade_guide)
|
|
self.assertNotIn("### License, Trial, and Entitlements", upgrade_guide)
|
|
self.assertIn("does not expose a general in-app trial, trial-return callback, or hosted AI quickstart", normalize_ws(upgrade_guide))
|
|
self.assertIn(
|
|
"Self-hosted monitoring and child-resource volume are not metered under the current v6 policy",
|
|
upgrade_guide,
|
|
)
|
|
self.assertIn("monitored-system, guest, or child-resource volume cap", upgrade_guide)
|
|
self.assertNotIn("uncapped monitored-system and guest capacity", upgrade_guide)
|
|
self.assertNotIn("uncapped capacity automatically", upgrade_guide)
|
|
self.assertNotIn("`POST /api/license/trial/start`", upgrade_guide)
|
|
self.assertNotIn("signed activation token to `/auth/trial-activate`", upgrade_guide)
|
|
self.assertNotIn("25 hosted Patrol", upgrade_guide)
|
|
stable_packet_paths = stable_packet_paths_for_version(current_version)
|
|
if stable_packet_paths is not None:
|
|
release_notes_path, changelog_path = stable_packet_paths
|
|
self.assertIn(release_notes_path, upgrade_guide)
|
|
self.assertIn(changelog_path, upgrade_guide)
|
|
for _, _, _, support_pack in discover_rc_draft_packets():
|
|
self.assertNotIn(support_pack, upgrade_guide)
|
|
self.assertNotIn("docs/releases/V6_RC_OPERATOR_SUPPORT_PACK.md", upgrade_guide)
|
|
else:
|
|
packet_paths = rc_packet_paths_for_version(current_version)
|
|
support_packet_paths = support_prerelease_packet_paths_for_version(current_version)
|
|
self.assertTrue(
|
|
packet_paths is not None or support_packet_paths is not None,
|
|
f"VERSION={current_version} does not match a governed v6 prerelease packet pattern",
|
|
)
|
|
if packet_paths is not None:
|
|
current_support_pack = packet_paths[2]
|
|
self.assertIn(current_support_pack, upgrade_guide)
|
|
for _, _, _, support_pack in discover_rc_draft_packets():
|
|
if support_pack == current_support_pack:
|
|
continue
|
|
self.assertNotIn(support_pack, upgrade_guide)
|
|
self.assertNotIn("docs/releases/V6_RC_OPERATOR_SUPPORT_PACK.md", upgrade_guide)
|
|
else:
|
|
assert support_packet_paths is not None
|
|
release_notes_path, changelog_path = support_packet_paths
|
|
self.assertIn(release_notes_path, upgrade_guide)
|
|
self.assertIn(changelog_path, upgrade_guide)
|
|
for _, _, _, support_pack in discover_rc_draft_packets():
|
|
self.assertNotIn(support_pack, upgrade_guide)
|
|
self.assertNotIn("docs/releases/V6_RC_OPERATOR_SUPPORT_PACK.md", upgrade_guide)
|
|
|
|
def test_prerelease_feedback_template_uses_generic_current_rc_wording(self) -> None:
|
|
template = read(".github/ISSUE_TEMPLATE/v6_rc_feedback.yml")
|
|
self.assertIn("placeholder: v6.x.y-rc.N", template)
|
|
self.assertIn("placeholder: rcourtman/pulse:v6.x.y-rc.N or rcourtman/pulse@sha256:...", template)
|
|
self.assertIn("I upgraded to the current v6 RC build", template)
|
|
self.assertNotIn("v6.0.0-rc.1", template)
|
|
|
|
def test_prerelease_intake_covers_stable_upgrades_and_separate_agent_versions(self) -> None:
|
|
form = yaml.load(read(".github/ISSUE_TEMPLATE/v6_rc_feedback.yml"), Loader=UniqueKeyLoader)
|
|
fields = {field["id"]: field for field in form["body"] if "id" in field}
|
|
self.assertEqual(len(fields), sum("id" in field for field in form["body"]))
|
|
self.assertEqual(fields["install_path"]["attributes"]["options"], [
|
|
"Clean v6 install", "Upgrade from v5", "Upgrade from a stable v6 release",
|
|
"Upgrade from an earlier v6 prerelease", "Not sure",
|
|
])
|
|
self.assertTrue(fields["install_path"]["validations"]["required"])
|
|
agent = fields["agent_version"]
|
|
self.assertEqual(agent["type"], "input")
|
|
self.assertEqual(agent["attributes"]["label"], "Agent version")
|
|
self.assertFalse(agent["validations"]["required"])
|
|
for distinction in ("affected agent", "separately from the Pulse server", '"none"',
|
|
'"unknown"', "Do not reinstall or re-enrol"):
|
|
self.assertIn(distinction, agent["attributes"]["description"])
|
|
self.assertIn("affected monitored platform and its release", fields["environment"]["attributes"]["description"])
|
|
|
|
def test_report_environment_keeps_platform_and_collection_context_optional(self) -> None:
|
|
for name in ("bug_report.yml", "v6_rc_feedback.yml"):
|
|
with self.subTest(form=name):
|
|
form = yaml.load(read(f".github/ISSUE_TEMPLATE/{name}"), Loader=UniqueKeyLoader)
|
|
fields = {field["id"]: field for field in form["body"] if "id" in field}
|
|
self.assertEqual(len(fields), sum("id" in field for field in form["body"]))
|
|
environment = fields["environment"]
|
|
self.assertEqual(environment["type"], "input")
|
|
self.assertEqual(environment["attributes"]["label"], "OS / environment")
|
|
self.assertFalse(environment["validations"]["required"])
|
|
self.assertNotIn("render", environment["attributes"])
|
|
prose = normalize_ws(environment["attributes"]["description"])
|
|
for distinction in (
|
|
"Pulse server OS", "affected monitored platform and its release",
|
|
"TrueNAS SCALE", "CORE", "platform API, a Pulse agent, or both",
|
|
"separate from the Pulse server's installation and version",
|
|
"Use existing settings or observations", 'leave blank or write "unknown"',
|
|
"Do not run diagnostics, probe, restart or change a connection",
|
|
):
|
|
self.assertIn(distinction, prose)
|
|
self.assertIn("via API, no Pulse agent", environment["attributes"]["placeholder"])
|
|
# Optional context must not relax the original report's core fields.
|
|
self.assertTrue(fields["pulse_version"]["validations"]["required"])
|
|
summary_id = "bug_description" if name == "bug_report.yml" else "summary"
|
|
self.assertTrue(fields[summary_id]["validations"]["required"])
|
|
for document in ("docs/ISSUE_TRIAGE.md", "frontend-modern/public/docs/ISSUE_TRIAGE.md"):
|
|
prose = normalize_ws(read(document))
|
|
for distinction in (
|
|
"optional **OS / environment**", "platform API, a Pulse agent, or both",
|
|
"TrueNAS SCALE versus CORE", "separately from the Pulse and agent versions",
|
|
"working agent view does not prove that the API view recovered",
|
|
'blank or "unknown" is valid', "Existing reports need no refile",
|
|
):
|
|
self.assertIn(distinction, prose)
|
|
|
|
def test_working_version_context_is_optional_and_never_requires_another_attempt(self) -> None:
|
|
for name in ("bug_report.yml", "v6_rc_feedback.yml"):
|
|
with self.subTest(form=name):
|
|
form = yaml.load(read(f".github/ISSUE_TEMPLATE/{name}"), Loader=UniqueKeyLoader)
|
|
fields = {field["id"]: field for field in form["body"] if "id" in field}
|
|
self.assertEqual(len(fields), sum("id" in field for field in form["body"]))
|
|
previous = fields["previous_version"]
|
|
self.assertEqual(previous["type"], "input")
|
|
self.assertEqual(previous["attributes"]["label"], "Last known working Pulse version")
|
|
self.assertFalse(previous["validations"]["required"])
|
|
for precaution in ("already observed the same behaviour working",
|
|
"not necessarily a working baseline", 'write "unknown"',
|
|
"do not downgrade, restart or repeat the action"):
|
|
self.assertIn(precaution, previous["attributes"]["description"])
|
|
self.assertEqual(fields["pulse_version"]["attributes"]["label"], "Pulse version")
|
|
self.assertTrue(fields["pulse_version"]["validations"]["required"])
|
|
self.assertIn('"unknown"', fields["agent_version"]["attributes"]["description"])
|
|
self.assertNotIn("render", previous["attributes"])
|
|
for document in ("docs/ISSUE_TRIAGE.md", "frontend-modern/public/docs/ISSUE_TRIAGE.md"):
|
|
triage = normalize_ws(read(document))
|
|
self.assertIn("an upgrade's starting version alone does not establish that", triage)
|
|
self.assertIn("do not ask for a downgrade, restart, reinstall or re-enrolment", triage)
|
|
|
|
def test_report_intake_safety_is_self_contained_for_installed_releases(self) -> None:
|
|
for name, evidence_id in (("bug_report.yml", "logs"), ("v6_rc_feedback.yml", "evidence")):
|
|
with self.subTest(form=name):
|
|
template = read(f".github/ISSUE_TEMPLATE/{name}")
|
|
form = yaml.safe_load(template)
|
|
introduction = form["body"][0]
|
|
self.assertEqual(introduction["type"], "markdown")
|
|
prose = introduction["attributes"]["value"]
|
|
self.assertNotRegex(
|
|
template,
|
|
r"https://github\.com/[^/\s)]+/[^/\s)]+/(?:blob|tree)/(?:main|master)/",
|
|
)
|
|
# The precautions must survive without following any link;
|
|
# putting them only in a link label is insufficient.
|
|
standalone = re.sub(r"\[[^\]]+\]\([^)]*\)", "", prose)
|
|
for precaution in (
|
|
"only collect diagnostics if Pulse is running and collection is safe",
|
|
"live API and guest-agent requests",
|
|
"do not run it during backups, freeze/thaw or an unresponsive-host incident",
|
|
"Prefer existing observations",
|
|
"downloads that result without running checks again",
|
|
"Review files and screenshots locally before posting",
|
|
"even when an export is labelled",
|
|
"session cookies",
|
|
"secret URLs",
|
|
"personal details",
|
|
"private keys",
|
|
"Copy as cURL",
|
|
"full network exports",
|
|
):
|
|
self.assertIn(precaution, standalone)
|
|
evidence = next(field for field in form["body"] if field.get("id") == evidence_id)
|
|
self.assertEqual(evidence["attributes"]["label"], "Logs, screenshots, or diagnostics")
|
|
self.assertFalse(evidence.get("validations", {}).get("required", False))
|
|
|
|
def test_notification_report_context_is_optional_and_passive(self) -> None:
|
|
for name, evidence_id in (("bug_report.yml", "logs"), ("v6_rc_feedback.yml", "evidence")):
|
|
with self.subTest(form=name):
|
|
form = yaml.load(read(f".github/ISSUE_TEMPLATE/{name}"), Loader=UniqueKeyLoader)
|
|
fields = {field["id"]: field for field in form["body"] if "id" in field}
|
|
self.assertEqual(len(fields), sum("id" in field for field in form["body"]))
|
|
evidence = fields[evidence_id]
|
|
self.assertEqual(evidence["type"], "textarea")
|
|
self.assertFalse(evidence.get("validations", {}).get("required", False))
|
|
self.assertNotIn("render", evidence["attributes"])
|
|
# Keep every distinction at the optional attachment point. The
|
|
# accepted form expands it there instead of overloading the
|
|
# field's short description or adding a required questionnaire.
|
|
context = expandable_intake_context(
|
|
form, evidence_id, "Notification delivery: Test versus ordinary alerts",
|
|
)
|
|
for distinction in (
|
|
"already-observed **Test** result", "ordinary alert delivery",
|
|
"single, grouped/digest or resolved", "destination type",
|
|
"built-in or custom template", "original time", "redacted error",
|
|
"missing host/resource context where known",
|
|
"successful Test does not establish ordinary delivery or correct identity",
|
|
"Use existing messages and queue details only", 'blank or "unknown" is valid',
|
|
"Do not send a Test, trigger an alert, retry/replay a queued message",
|
|
"clear the queue or change notification settings just to complete this report",
|
|
"Keep destination addresses, webhook URLs, chat IDs, tokens",
|
|
"full notification settings or payloads private",
|
|
):
|
|
self.assertIn(distinction, context)
|
|
self.assertTrue(fields["pulse_version"]["validations"]["required"])
|
|
|
|
def test_expandable_intake_context_cannot_be_hidden_or_detached(self) -> None:
|
|
for name, summary_id, evidence_id in (
|
|
("bug_report.yml", "bug_description", "logs"),
|
|
("v6_rc_feedback.yml", "summary", "evidence"),
|
|
):
|
|
for field_id, heading in (
|
|
(summary_id, "Several hosts or clusters: preserve which names and IDs repeat"),
|
|
(evidence_id, "Notification delivery: Test versus ordinary alerts"),
|
|
):
|
|
with self.subTest(form=name, field=field_id):
|
|
form = yaml.load(read(f".github/ISSUE_TEMPLATE/{name}"), Loader=UniqueKeyLoader)
|
|
context = expandable_intake_context(form, field_id, heading)
|
|
self.assertTrue(context)
|
|
index = next(i for i, field in enumerate(form["body"]) if field.get("id") == field_id)
|
|
original = form["body"][index - 1]
|
|
value = original["attributes"]["value"]
|
|
for change in (
|
|
{"type": "textarea", "attributes": {"description": value}},
|
|
{"type": "markdown", "attributes": {"value": value.replace("<details>", "", 1)}},
|
|
{"type": "markdown", "attributes": {"value": value.replace("</details>", "", 1)}},
|
|
{"type": "markdown", "attributes": {"value": value.replace(heading, "Other context", 1)}},
|
|
{"type": "markdown", "attributes": {"value": value + "\n" + value}},
|
|
{"type": "markdown", "attributes": {"value": "See the introduction."}},
|
|
):
|
|
# Keep the original elsewhere: a whole-form substring
|
|
# search would wrongly accept this detached context.
|
|
form["body"][index - 1] = change
|
|
form["body"].append(original)
|
|
with self.assertRaises(ValueError):
|
|
expandable_intake_context(form, field_id, heading)
|
|
form["body"].pop()
|
|
form["body"][index - 1] = original
|
|
self.assertEqual(expandable_intake_context(form, field_id, heading), context)
|
|
|
|
def test_notification_triage_separates_evaluation_delivery_and_identity(self) -> None:
|
|
documents = ("docs/ISSUE_TRIAGE.md", "frontend-modern/public/docs/ISSUE_TRIAGE.md")
|
|
self.assertEqual(read(documents[0]), read(documents[1]))
|
|
for document in documents:
|
|
with self.subTest(document=document):
|
|
context = normalize_ws(read(document).split("For notification reports,", 1)[1]
|
|
.split("## Required disposition", 1)[0])
|
|
for distinction in (
|
|
"successful Test does not establish ordinary delivery or correct identity",
|
|
"consistent private aliases", "alert appearing in Pulse",
|
|
"queued attempt", "recipient's actual message as separate observations",
|
|
"subject or body still identifies the wrong resource",
|
|
"including earlier comments", "rather than asking for the same facts again",
|
|
"Queued messages can retain older settings",
|
|
"current configuration screenshot does not establish",
|
|
"Unknown evidence stays unknown", "existing reports need no refile",
|
|
"Do not request another Test, induced alert, queue retry/replay",
|
|
"queue clearing or notification-setting changes",
|
|
"Keep destination addresses, webhook URLs, chat IDs, tokens",
|
|
"full notification settings or payloads private",
|
|
"missing notification alone does not prove that alert evaluation failed",
|
|
"retain both symptoms",
|
|
):
|
|
self.assertIn(distinction, context)
|
|
|
|
def test_report_intake_distinguishes_server_install_from_affected_target(self) -> None:
|
|
for name, summary_id, install_id in (
|
|
("bug_report.yml", "bug_description", "install_type"),
|
|
("v6_rc_feedback.yml", "summary", "installation_type"),
|
|
):
|
|
with self.subTest(form=name):
|
|
form = yaml.load(read(f".github/ISSUE_TEMPLATE/{name}"), Loader=UniqueKeyLoader)
|
|
fields = {field["id"]: field for field in form["body"] if "id" in field}
|
|
self.assertEqual(len(fields), sum("id" in field for field in form["body"]))
|
|
install = fields[install_id]
|
|
self.assertEqual(install["type"], "dropdown")
|
|
self.assertEqual(install["attributes"]["label"], "Installation type")
|
|
self.assertIn("Pulse server", install["attributes"]["description"])
|
|
self.assertIn("not the kind of workload", install["attributes"]["description"])
|
|
summary = fields[summary_id]
|
|
self.assertEqual(summary["type"], "textarea")
|
|
self.assertTrue(summary["validations"]["required"])
|
|
prose = normalize_ws(summary["attributes"]["description"])
|
|
for distinction in (
|
|
"affected target", "VM, LXC, Docker container, NAS or Pulse itself",
|
|
"from how the Pulse server is installed", "For missing readings",
|
|
"workload's usual UI", "when the problem occurred",
|
|
"Use existing observations", 'write "unknown" if you cannot safely tell',
|
|
"Do not run another update, backup or guest-agent probe just to answer",
|
|
):
|
|
self.assertIn(distinction, prose)
|
|
self.assertNotIn("render", summary["attributes"])
|
|
|
|
for document in (
|
|
"docs/ISSUE_TRIAGE.md",
|
|
"frontend-modern/public/docs/ISSUE_TRIAGE.md",
|
|
):
|
|
with self.subTest(document=document):
|
|
triage = normalize_ws(read(document))
|
|
for precaution in (
|
|
"installation type does not make Docker commands relevant",
|
|
"Read later comments", "retain both topics",
|
|
"not Pulse's displayed connection status",
|
|
"Responsiveness is not proof that every filesystem is writable",
|
|
'Accept "unknown"', "Existing reports need no refile",
|
|
):
|
|
self.assertIn(precaution, triage)
|
|
|
|
def test_cross_installation_intake_preserves_relationships_without_private_identity(self) -> None:
|
|
for name, summary_id in (
|
|
("bug_report.yml", "bug_description"),
|
|
("v6_rc_feedback.yml", "summary"),
|
|
):
|
|
with self.subTest(form=name):
|
|
form = yaml.load(read(f".github/ISSUE_TEMPLATE/{name}"), Loader=UniqueKeyLoader)
|
|
fields = {field["id"]: field for field in form["body"] if "id" in field}
|
|
self.assertEqual(len(fields), sum("id" in field for field in form["body"]))
|
|
summary = fields[summary_id]
|
|
self.assertEqual(summary["type"], "textarea")
|
|
self.assertTrue(summary["validations"]["required"])
|
|
self.assertNotIn("render", summary["attributes"])
|
|
prose = expandable_intake_context(
|
|
form, summary_id, "Several hosts or clusters: preserve which names and IDs repeat",
|
|
)
|
|
for distinction in (
|
|
"For reports involving several hosts or clusters",
|
|
"which nodes share a cluster", "independent installations",
|
|
"whether node names or guest IDs (VMIDs) repeat",
|
|
"share a backup destination", "consistent aliases for private names and addresses",
|
|
"across the description, screenshots and logs", "preserve which values repeat",
|
|
"cluster A/node 1 and standalone B/node 1", "Use existing observations",
|
|
'write "unknown"', "do not add, rename, remove or re-enrol anything just to answer",
|
|
):
|
|
self.assertIn(distinction, prose)
|
|
# The conditional context belongs in the existing report, not a new
|
|
# required identity field or a request to collect a configuration.
|
|
self.assertNotIn("topology", fields)
|
|
self.assertNotIn("hostname", fields)
|
|
self.assertFalse(fields["environment"]["validations"]["required"])
|
|
|
|
documents = ("docs/ISSUE_TRIAGE.md", "frontend-modern/public/docs/ISSUE_TRIAGE.md")
|
|
self.assertEqual(read(documents[0]), read(documents[1]))
|
|
for document in documents:
|
|
with self.subTest(document=document):
|
|
prose = normalize_ws(read(document))
|
|
for distinction in (
|
|
"which nodes share a cluster", "independent installations",
|
|
"whether node names or guest IDs (VMIDs) repeat", "share a backup destination",
|
|
"Display names and VMIDs are not globally unique",
|
|
"distinct Pulse display name alone does not prove",
|
|
"consistent aliases for private names and addresses",
|
|
"preserving which values repeat", "same native node name",
|
|
"Do not infer that a backup or agent belongs",
|
|
"duplicate just because those identifiers overlap",
|
|
"full thread and attachments", "Unknown relationships remain unknown",
|
|
"do not ask for public hostnames, addresses or a configuration dump",
|
|
"add, rename, remove or re-enrol anything", "Existing reports need no refile",
|
|
"node errors, backup status and Docker monitoring",
|
|
"recovery in one does not establish recovery in the others",
|
|
"connection Host/URL, Pulse display name, native node/guest name",
|
|
"agent-reported hostname separate",
|
|
"full connection hostname does not establish",
|
|
"guest or agent names are fully qualified",
|
|
"ask only for the remembered short/full form",
|
|
"whether full domain suffixes differ, not the literal names",
|
|
"which guest has an agent", "whether it remained connected",
|
|
"shared guest name unique across the platform inventory",
|
|
"link is not proof that agent reporting stopped",
|
|
"Do not install another agent, change a link or recreate the addition",
|
|
"does not establish a fix for repeated short names",
|
|
):
|
|
self.assertIn(distinction, prose)
|
|
|
|
def test_demo_site_copy_points_at_current_release_packet_index(self) -> None:
|
|
demo_copy = read("docs/releases/V6_RC_DEMO_SITE_COPY.md")
|
|
self.assertIn("docs/RELEASE_NOTES.md", demo_copy)
|
|
self.assertIn("Current RC packet: `docs/releases/`", demo_copy)
|
|
self.assertNotIn("docs/releases/RELEASE_NOTES_v6.md", demo_copy)
|
|
self.assertNotIn("docs/releases/V6_RC_OPERATOR_SUPPORT_PACK.md", demo_copy)
|
|
|
|
def test_signpath_test_signing_workflow_is_non_publishing_and_test_only(self) -> None:
|
|
workflow = read(".github/workflows/signpath-test-signing.yml")
|
|
|
|
self.assertIn("workflow_dispatch:", workflow)
|
|
self.assertNotIn("pull_request:", workflow)
|
|
self.assertNotIn("schedule:", workflow)
|
|
self.assertNotIn("push:", workflow)
|
|
self.assertIn("SignPath Test Signing Proof (Never Publish)", workflow)
|
|
self.assertIn("if ($env:GITHUB_REF_NAME -ne 'main')", workflow)
|
|
self.assertNotIn("'${{ inputs.version }}'", workflow)
|
|
self.assertIn("does not match repository VERSION", workflow)
|
|
self.assertIn("signing-policy-slug: test-signing", workflow)
|
|
self.assertNotIn(
|
|
"signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }}",
|
|
workflow,
|
|
)
|
|
self.assertIn(
|
|
"The canonical SIGNPATH_SIGNING_POLICY_SLUG must remain release-signing.",
|
|
workflow,
|
|
)
|
|
self.assertIn(
|
|
"signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0",
|
|
workflow,
|
|
)
|
|
self.assertIn("signedArtifactsPublished = $false", workflow)
|
|
self.assertIn("signedArtifactsUploadedAsGitHubArtifact = $false", workflow)
|
|
self.assertIn("nonProduction = $true", workflow)
|
|
self.assertIn("WaitForExit(90000)", workflow)
|
|
self.assertIn("System32\\certutil.exe", workflow)
|
|
self.assertIn("WaitForExit(30000)", workflow)
|
|
self.assertIn("certutil timed out after 30 seconds", workflow)
|
|
self.assertIn("'-addstore'", workflow)
|
|
self.assertIn("'-delstore'", workflow)
|
|
self.assertNotIn("'-user'", workflow)
|
|
self.assertIn(
|
|
"ephemeralTestTrustScope = 'LocalMachine/Root and LocalMachine/TrustedPublisher'",
|
|
workflow,
|
|
)
|
|
self.assertNotIn("X509Store]::new", workflow)
|
|
self.assertNotIn("Import-Certificate", workflow)
|
|
self.assertIn("path: signpath-test-signing-evidence.json", workflow)
|
|
self.assertEqual(
|
|
workflow.count(
|
|
"uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1"
|
|
),
|
|
2,
|
|
)
|
|
self.assertNotIn("path: signpath-test-output", workflow)
|
|
self.assertNotIn("gh release", workflow)
|
|
self.assertNotIn("release-candidate", workflow)
|
|
for name in (
|
|
"pulse-agent-windows-amd64.exe",
|
|
"pulse-agent-windows-arm64.exe",
|
|
"pulse-agent-windows-386.exe",
|
|
):
|
|
self.assertIn(name, workflow)
|
|
|
|
def test_update_demo_server_workflow_uses_stable_tag_example(self) -> None:
|
|
workflow = read(".github/workflows/update-demo-server.yml")
|
|
self.assertIn("Stable release tag to deploy", workflow)
|
|
self.assertIn("Prerelease demo updates are retired after v6 GA", workflow)
|
|
self.assertNotIn("v6.0.0-rc.1", workflow)
|
|
|
|
def test_rehearsal_template_and_workflow_capture_ga_rehearsal_record(self) -> None:
|
|
template = read("docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md")
|
|
workflow = read(".github/workflows/release-dry-run.yml")
|
|
release_workflow = read(".github/workflows/create-release.yml")
|
|
dry_run_trigger = read("scripts/trigger-release-dry-run.sh")
|
|
recorder = read("scripts/release_control/record_rc_to_ga_rehearsal.py")
|
|
internal_recorder = read("scripts/release_control/internal/record_rc_to_ga_rehearsal.py")
|
|
renderer = read("scripts/release_control/render_release_body.py")
|
|
generator = read("scripts/generate-release-notes.sh")
|
|
release_notes_template = read("docs/releases/RELEASE_NOTES_TEMPLATE.md")
|
|
resolver = read("scripts/release_control/resolve_release_promotion.py")
|
|
self.assertIn("GitHub Actions run URL", template)
|
|
self.assertIn("Exact GA date to publish with GA", template)
|
|
self.assertIn("record_rc_to_ga_rehearsal.py --run-id <run-id>", template)
|
|
self.assertIn("rc-to-ga-promotion-readiness-rehearsal-<record-date>.md", template)
|
|
self.assertIn(promotion_metadata_envelope(), normalize_ws(template))
|
|
self.assertIn("rc-to-ga-rehearsal-summary", workflow)
|
|
self.assertIn("build_release_candidate:", workflow)
|
|
self.assertIn("if: ${{ inputs.watchdog != true && inputs.version != '' }}", workflow)
|
|
self.assertIn("require_macos_signing: true", workflow)
|
|
self.assertIn(
|
|
"require_windows_signing: false",
|
|
workflow,
|
|
)
|
|
self.assertIn("WINDOWS_AUTHENTICODE_AVAILABLE", workflow)
|
|
self.assertIn("unsigned_windows_exception:", workflow)
|
|
self.assertIn("unsigned_windows_reason:", workflow)
|
|
self.assertIn("windows_signing_backend: signpath", workflow)
|
|
self.assertRegex(
|
|
workflow,
|
|
r"(?ms)^ build_release_candidate:\n.*?^ permissions:\n"
|
|
r" actions: write\n attestations: write\n contents: read\n"
|
|
r" id-token: write\n uses: \.\/\.github\/workflows\/build-release-candidate\.yml$",
|
|
)
|
|
self.assertRegex(
|
|
release_workflow,
|
|
r"(?ms)^ build_release_candidate:\n.*?^ permissions:\n"
|
|
r" actions: write\n attestations: write\n contents: read\n"
|
|
r" id-token: write\n uses: \.\/\.github\/workflows\/build-release-candidate\.yml$",
|
|
)
|
|
self.assertIn("Definitive Dry-Run Verdict", workflow)
|
|
self.assertIn('require_result "exact-SHA release candidate" "$CANDIDATE_RESULT" success', workflow)
|
|
self.assertIn('require_result "stable demo no-mutation verification" "$DEMO_RESULT" success', workflow)
|
|
self.assertNotIn("if: ${{ github.event_name == 'workflow_dispatch' }}", workflow)
|
|
self.assertIn("record_rc_to_ga_rehearsal.py --run-id ${{ github.run_id }}", workflow)
|
|
self.assertIn("rc-to-ga-promotion-readiness-rehearsal-<record-date>.md", workflow)
|
|
self.assertIn("control_plane.py --branch-for-version", workflow)
|
|
self.assertIn('git fetch --prune origin main "${REQUIRED_BRANCH}" --tags', workflow)
|
|
self.assertIn("resolve_release_promotion.py", workflow)
|
|
self.assertIn("- Rollback command:", workflow)
|
|
self.assertIn("- Candidate stable tag:", workflow)
|
|
self.assertIn("- Promotion channel:", workflow)
|
|
self.assertIn("- Promoted prerelease tag:", workflow)
|
|
self.assertIn("Prerelease soak hours at rehearsal time", workflow)
|
|
self.assertIn("Planned GA date", workflow)
|
|
self.assertIn("Planned v5 end-of-support date", workflow)
|
|
self.assertIn("go test -p 1 ./...", workflow)
|
|
self.assertIn("2-core hosted runners", workflow)
|
|
self.assertIn("resolve_release_promotion.py", release_workflow)
|
|
self.assertIn("render_release_body.py", release_workflow)
|
|
self.assertIn("build_rollback_section", renderer)
|
|
self.assertIn("# Pulse v${VERSION} Release Notes", generator)
|
|
self.assertIn("## What's improved", generator)
|
|
self.assertIn("## What's improved", release_notes_template)
|
|
self.assertIn("promotion-metadata", release_notes_template)
|
|
self.assertIn("default_output_path", internal_recorder)
|
|
self.assertIn("output path already exists", internal_recorder)
|
|
self.assertIn("default_output_path", recorder)
|
|
self.assertIn("rollback_version is required for every release rehearsal and promotion", resolver)
|
|
self.assertIn("Stable promotion requires promoted_from_tag", resolver)
|
|
self.assertIn("Only governed stable patch releases may use the routine no-RC path.", resolver)
|
|
self.assertIn("Stable v6.0.0 requires ga_date in YYYY-MM-DD form", resolver)
|
|
self.assertIn("release_notes must include the exact ga_date", resolver)
|
|
self.assertIn("check-workflow-dispatch-inputs.py", dry_run_trigger)
|
|
self.assertIn('--branch "$CURRENT_BRANCH"', dry_run_trigger)
|
|
self.assertIn("release-dry-run.yml", dry_run_trigger)
|
|
self.assertIn("gh workflow run release-dry-run.yml", dry_run_trigger)
|
|
self.assertIn("Release Dry Run executes the selected remote ref", dry_run_trigger)
|
|
self.assertIn("Hotfix exception to bypass 24-hour prerelease soak? [y/N]", dry_run_trigger)
|
|
self.assertIn("blank only for approved hotfix", dry_run_trigger)
|
|
self.assertIn('if [ -z "$PROMOTED_FROM_TAG" ] && [ "$HOTFIX_EXCEPTION" != "true" ]; then', dry_run_trigger)
|
|
self.assertNotIn("Continue anyway?", dry_run_trigger)
|
|
self.assertIn('if [ "${REHEARSAL_CONCLUSION}" != "success" ]; then', workflow)
|
|
self.assertIn("did not produce a valid promotion metadata envelope", workflow)
|
|
self.assertIn("Do not use this artifact to clear", workflow)
|
|
|
|
def test_release_workflow_enforces_rc_lineage_soak_and_v5_notice(self) -> None:
|
|
content = read(".github/workflows/create-release.yml")
|
|
update_demo_workflow = read(".github/workflows/update-demo-server.yml")
|
|
deploy_demo_workflow = read(".github/workflows/deploy-demo-server.yml")
|
|
demo_ssh_helper = read(".github/scripts/setup-demo-ssh.sh")
|
|
demo_reachability_helper = read(".github/scripts/check-demo-reachability.sh")
|
|
validation_workflow = read(".github/workflows/validate-release-assets.yml")
|
|
candidate_workflow = read(".github/workflows/build-release-candidate.yml")
|
|
compiler_workflow = read(".github/workflows/compile-release-payload.yml")
|
|
qualifier_workflow = read(".github/workflows/qualify-release-containers.yml")
|
|
docker_build = workflow_job_block(qualifier_workflow, "qualify")
|
|
release_validator = read("scripts/validate-release.sh")
|
|
helper = read("scripts/trigger-release.sh")
|
|
stable_patch_helper = read("scripts/trigger-stable-patch.sh")
|
|
renderer = read("scripts/release_control/render_release_body.py")
|
|
policy = read("docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md")
|
|
source_of_truth = read("docs/release-control/v6/internal/SOURCE_OF_TRUTH.md")
|
|
runbook = read("docs/releases/V6_PRERELEASE_RUNBOOK.md")
|
|
resolver = read("scripts/release_control/resolve_release_promotion.py")
|
|
dry_run_workflow = read(".github/workflows/release-dry-run.yml")
|
|
dry_run_helper = read("scripts/trigger-release-dry-run.sh")
|
|
contract = read("docs/release-control/v6/internal/subsystems/deployment-installability.md")
|
|
self.assertIn("It does not automatically check out or build `pulse-enterprise`.", runbook)
|
|
self.assertIn("public `pulse-v...` release archives are OSS runtime artifacts", runbook)
|
|
self.assertIn("`pulse-pro-v...` archives", runbook)
|
|
self.assertIn("`bin/pulse --version` identifies `Pulse Pro`", runbook)
|
|
self.assertIn("Paid-user GA is part of that same release boundary", contract)
|
|
self.assertIn(
|
|
"the public Pulse release workflow builds OSS `pulse-v...` artifacts only",
|
|
normalize_ws(contract),
|
|
)
|
|
self.assertIn("`pulse-pro-v...` archives identify `Pulse Pro`", normalize_ws(contract))
|
|
self.assertIn("https://pulserelay.pro/download.html", contract)
|
|
self.assertIn("PULSE_IMAGE`-aware compose image line", normalize_ws(contract))
|
|
self.assertIn("hardcoded `image: rcourtman/pulse:...`", normalize_ws(contract))
|
|
self.assertIn("Resolving governed promotion metadata...", helper)
|
|
self.assertIn("--release-notes-file \"$NOTES_FILE\"", helper)
|
|
self.assertIn("--validate-notes-file \"$NOTES_FILE\"", helper)
|
|
self.assertIn("--rawfile release_notes \"$NOTES_FILE\"", helper)
|
|
self.assertIn("gh workflow run create-release.yml --ref \"$CURRENT_BRANCH\" --json", helper)
|
|
self.assertIn('--arg hotfix_exception "$HOTFIX_EXCEPTION"', helper)
|
|
self.assertIn(
|
|
'--arg unsigned_windows_exception "$UNSIGNED_WINDOWS_EXCEPTION"',
|
|
helper,
|
|
)
|
|
self.assertIn('--arg draft_only "false"', helper)
|
|
self.assertNotIn("--argjson", helper)
|
|
self.assertNotIn('-f release_notes="$(cat "$NOTES_FILE")"', helper)
|
|
self.assertIn("--validate-notes-file \"$NOTES_FILE\"", stable_patch_helper)
|
|
self.assertIn("--rawfile release_notes \"$NOTES_FILE\"", stable_patch_helper)
|
|
self.assertIn("gh workflow run create-release.yml --ref \"$CURRENT_BRANCH\" --json", stable_patch_helper)
|
|
self.assertIn("blank for hotfix with no RC lineage", helper)
|
|
self.assertIn('if [ -z "$PROMOTED_FROM_TAG" ] && [ "$HOTFIX_EXCEPTION" != "true" ]; then', helper)
|
|
self.assertIn("control_plane.py --branch-for-version", content)
|
|
self.assertIn('git fetch --prune origin main "${REQUIRED_BRANCH}" --tags', content)
|
|
self.assertIn('REQUIRED_BRANCH: ${{ steps.branch_policy.outputs.required_branch }}', content)
|
|
self.assertIn("resolve_release_promotion.py", content)
|
|
self.assertIn("release_stage: ${{ steps.promotion.outputs.release_stage }}", content)
|
|
self.assertIn("--enforce-prerelease-observation-window", content)
|
|
self.assertIn("DRAFT_ONLY_INPUT", content)
|
|
self.assertIn("--enforce-prerelease-observation-window", helper)
|
|
self.assertNotIn("--enforce-prerelease-observation-window", dry_run_workflow)
|
|
self.assertNotIn("--enforce-prerelease-observation-window", dry_run_helper)
|
|
self.assertIn("render_release_body.py", content)
|
|
self.assertIn('WORKFLOW_OUTPUT_3: ${{ needs.prepare.outputs.release_stage }}', content)
|
|
self.assertIn('--promotion-channel "${WORKFLOW_OUTPUT_3}"', content)
|
|
self.assertIn(
|
|
"needs.prepare.outputs.release_stage != 'alpha' && needs.prepare.outputs.release_stage != 'beta'",
|
|
content,
|
|
)
|
|
self.assertIn("build_rollback_section", renderer)
|
|
self.assertIn("uses: ./.github/workflows/publish-docker.yml", content)
|
|
self.assertIn("release-convergence.yml/dispatches", content)
|
|
self.assertIn("Release Activation Commit Verdict", content)
|
|
self.assertNotIn('gh workflow run publish-docker.yml --ref "${REQUIRED_BRANCH}"', content)
|
|
self.assertNotIn('gh workflow run update-demo-server.yml --ref "${REQUIRED_BRANCH}"', content)
|
|
self.assertIn("sanitize_release_notes", renderer)
|
|
self.assertIn("validate_release_notes_shape", renderer)
|
|
self.assertIn("validate_release_body_shape", renderer)
|
|
self.assertIn("GitHub's stored release body does not exactly match", renderer)
|
|
self.assertIn("Do not treat this as published", renderer)
|
|
self.assertIn("_DRAFT.md", renderer)
|
|
self.assertIn("rollback target and exact reinstall command recorded", policy)
|
|
self.assertIn("rc-to-ga-rehearsal-summary", policy)
|
|
self.assertIn("record_rc_to_ga_rehearsal.py --run-id <run-id>", policy)
|
|
self.assertIn(promotion_metadata_envelope(), normalize_ws(policy))
|
|
self.assertIn("recorded rollback target plus exact", source_of_truth)
|
|
self.assertIn("hours of prerelease soak", resolver)
|
|
self.assertIn("minimum is {MIN_STABLE_SOAK_HOURS} hours unless hotfix_exception is true", resolver)
|
|
self.assertIn("MIN_PRERELEASE_OBSERVATION_HOURS = 24", resolver)
|
|
self.assertIn("cohort checkpoint, not a delivery vehicle", normalize_ws(policy))
|
|
self.assertIn("at least 24 hours of public observation", normalize_ws(source_of_truth))
|
|
self.assertIn(
|
|
"Public checkpoints at the same maturity stage on one version line are separated by at least 24 hours",
|
|
normalize_ws(contract),
|
|
)
|
|
self.assertIn("build_rollback_section", renderer)
|
|
self.assertIn("promotion metadata out of customer notes", renderer)
|
|
self.assertIn("historical_asset_backfill_only:", content)
|
|
self.assertIn("expected_source_sha:", content)
|
|
self.assertIn('EXPECTED_SOURCE_SHA: ${{ inputs.expected_source_sha }}', content)
|
|
self.assertIn('"${GITHUB_SHA}" != "${EXPECTED_SOURCE_SHA}"', content)
|
|
self.assertIn('"${GITHUB_WORKFLOW_SHA}" != "${EXPECTED_SOURCE_SHA}"', content)
|
|
self.assertIn("Repair an already-published release packet in place without rebuilding binaries", content)
|
|
self.assertIn("draft: true", content)
|
|
self.assertIn("activate_release:", content)
|
|
self.assertIn("Publish the fully staged release", content)
|
|
self.assertIn('gh api "repos/${{ github.repository }}/releases?per_page=100" --paginate', content)
|
|
self.assertNotIn('git push origin "refs/tags/${TAG}" --force', content)
|
|
self.assertNotIn('Retargeting existing draft tag', content)
|
|
self.assertIn('Public tag ${TAG} already identifies', content)
|
|
self.assertIn('Resuming quarantined draft release for ${TAG}', content)
|
|
self.assertIn(
|
|
'write_github_output.py release_activation_committed "${RELEASE_ACTIVATION_COMMITTED}"',
|
|
content,
|
|
)
|
|
self.assertIn('[ "$ACTIVATION_COMMITTED" != "true" ]', content)
|
|
self.assertNotIn('[ -z "$EXISTING_RELEASE_PUBLISHED_AT" ]', content)
|
|
self.assertNotIn('[ -z "$PUBLISHED_AT" ]', content)
|
|
self.assertIn('--rawfile body "$NOTES_FILE"', content)
|
|
self.assertIn('--input "$RELEASE_PAYLOAD"', content)
|
|
self.assertIn('--expected-body-file "$NOTES_FILE"', content)
|
|
self.assertIn(
|
|
'write_github_output.py historical_asset_backfill_only "${HISTORICAL_ASSET_BACKFILL_ONLY}"',
|
|
content,
|
|
)
|
|
self.assertIn(
|
|
"if: ${{ always() && needs.prepare.result == 'success' && needs.build_release_candidate.result == 'success' && needs.create_release.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' }}",
|
|
content,
|
|
)
|
|
self.assertIn("candidate_manifest_artifact:", validation_workflow)
|
|
self.assertIn("release_candidate_manifest.py verify-release", validation_workflow)
|
|
self.assertIn('--release-body-file "$RUNNER_TEMP/release-body.md"', validation_workflow)
|
|
self.assertIn("VALIDATION_EXIT_CODE=${PIPESTATUS[0]}", validation_workflow)
|
|
self.assertIn("if: ${{ needs.prepare.outputs.historical_asset_backfill_only == 'true' }}", content)
|
|
self.assertIn("issues: write", content)
|
|
self.assertIn("statuses: write", content)
|
|
self.assertIn("statuses: write", validation_workflow)
|
|
self.assertIn("curl --fail-with-body --silent --show-error -X POST", validation_workflow)
|
|
self.assertIn('"context": "Release Asset Validation"', validation_workflow)
|
|
self.assertIn('WORKFLOW_OUTPUT_4: ${{ steps.context.outputs.tag }}', validation_workflow)
|
|
self.assertIn(
|
|
'WORKFLOW_OUTPUT_5: ${{ steps.context.outputs.target_commitish }}',
|
|
validation_workflow,
|
|
)
|
|
self.assertIn('--arg tag "${WORKFLOW_OUTPUT_4}"', validation_workflow)
|
|
self.assertIn('--arg target_commitish "${WORKFLOW_OUTPUT_5}"', validation_workflow)
|
|
self.assertIn("{body: $body, tag_name: $tag, target_commitish: $target_commitish}", validation_workflow)
|
|
self.assertIn("{draft: true, tag_name: $tag, target_commitish: $target_commitish}", validation_workflow)
|
|
self.assertIn("Validation release body update detached release tag", validation_workflow)
|
|
self.assertIn("Validation release body update changed target_commitish", validation_workflow)
|
|
self.assertIn("--validate-body-file \"$RELEASE_BODY_FILE\"", validation_workflow)
|
|
self.assertIn("--expected-body-file \"$CLEAN_BODY_FILE\"", validation_workflow)
|
|
self.assertIn("Quarantine malformed release body", validation_workflow)
|
|
self.assertIn(
|
|
"Draft releases are quarantined; published releases remain immutable for explicit remediation.",
|
|
validation_workflow,
|
|
)
|
|
for step in (
|
|
"Quarantine malformed release body",
|
|
"Update release body - Success",
|
|
"Delete all release assets on failure",
|
|
"Update release body - Failure",
|
|
):
|
|
self.assertIn(
|
|
f"- name: {step}\n"
|
|
" if: steps.context.outputs.should_run == 'true' && "
|
|
"steps.context.outputs.draft == 'true'",
|
|
validation_workflow,
|
|
)
|
|
self.assertNotIn(
|
|
"Release was published; reverting to draft before deleting assets",
|
|
validation_workflow,
|
|
)
|
|
self.assertNotIn(
|
|
"A published release edit introduced invalid assets",
|
|
validation_workflow,
|
|
)
|
|
self.assertIn(
|
|
'ACTUAL_RELEASE_TAG=$(jq -r \'.tag_name // empty\' "$RELEASE_JSON_FILE")',
|
|
content,
|
|
)
|
|
self.assertIn(
|
|
'ACTUAL_TARGET_COMMITISH=$(jq -r \'.target_commitish // empty\' "$RELEASE_JSON_FILE")',
|
|
content,
|
|
)
|
|
self.assertIn('WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}', content)
|
|
self.assertIn('./scripts/backfill-release-assets.sh --tag "${WORKFLOW_OUTPUT_1}" --repo "${{ github.repository }}"', content)
|
|
self.assertIn('./scripts/validate-published-release.sh "${WORKFLOW_OUTPUT_1}" "${{ github.repository }}"', content)
|
|
self.assertIn("PULSE_UPDATE_SIGNING_KEY: ${{ secrets.PULSE_UPDATE_SIGNING_KEY }}", content)
|
|
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}", content)
|
|
self.assertIn(
|
|
normalize_ws(
|
|
"""
|
|
- name: Validate published release packet
|
|
env:
|
|
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
|
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}
|
|
run: |
|
|
./scripts/validate-published-release.sh "${WORKFLOW_OUTPUT_1}" "${{ github.repository }}"
|
|
"""
|
|
),
|
|
normalize_ws(content),
|
|
)
|
|
self.assertNotIn("pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}", docker_build)
|
|
self.assertIn("Validate installer signing key pins", candidate_workflow)
|
|
self.assertIn("timeout-minutes: 60", candidate_workflow)
|
|
self.assertNotRegex(candidate_workflow, r"(?m)^\s+runs-on:.*self-hosted")
|
|
self.assertIn("Compile Exact-SHA Release Payload on Ephemeral VM", compiler_workflow)
|
|
self.assertIn("runs-on: ubuntu-24.04", compiler_workflow)
|
|
self.assertNotRegex(compiler_workflow, r"(?m)^\s+runs-on:.*self-hosted")
|
|
self.assertNotIn("pulse-pve-", compiler_workflow)
|
|
self.assertIn("GITHUB_WORKFLOW_SHA", compiler_workflow)
|
|
self.assertIn("ref: ${{ inputs.source_sha }}", compiler_workflow)
|
|
self.assertIn("PULSE_RELEASE_BUILD_JOBS: \"2\"", compiler_workflow)
|
|
self.assertIn("return_run_details: true", candidate_workflow)
|
|
self.assertIn("actions/workflows/compile-release-payload.yml/dispatches", candidate_workflow)
|
|
self.assertIn("X-GitHub-Api-Version: 2026-03-10", candidate_workflow)
|
|
self.assertIn("compiler_run_id: ${{ steps.dispatch.outputs.compiler_run_id }}", candidate_workflow)
|
|
self.assertIn('.path == ".github/workflows/compile-release-payload.yml"', candidate_workflow)
|
|
self.assertIn(
|
|
"EXPECTED_ARTIFACT_ID: ${{ needs.obtain-release-payload.outputs.artifact_id }}",
|
|
candidate_workflow,
|
|
)
|
|
self.assertIn(
|
|
"EXPECTED_ARTIFACT_DIGEST: ${{ needs.obtain-release-payload.outputs.artifact_digest }}",
|
|
candidate_workflow,
|
|
)
|
|
self.assertIn(
|
|
"EXPECTED_COMPILER_RUN_ID: ${{ needs.obtain-release-payload.outputs.compiler_run_id }}",
|
|
candidate_workflow,
|
|
)
|
|
self.assertIn(".workflow_run.head_sha == $source_sha", candidate_workflow)
|
|
self.assertIn("sha256sum --check --", candidate_workflow)
|
|
self.assertIn("compiled-payload-verification.json", candidate_workflow)
|
|
self.assertIn("separate-ephemeral-github-hosted-compiler-workflow", candidate_workflow)
|
|
self.assertIn("Verify Native Signing Configuration", candidate_workflow)
|
|
self.assertEqual(candidate_workflow.count("needs: signing-configuration"), 2)
|
|
self.assertIn("require_windows_signing: ${{ needs.prepare.outputs.require_windows_signing == 'true' }}", content)
|
|
self.assertIn("unsigned_windows_exception:", content)
|
|
self.assertIn("unsigned_windows_reason:", content)
|
|
self.assertIn("WINDOWS_AUTHENTICODE_AVAILABLE = False", resolver)
|
|
self.assertIn("WINDOWS_AUTHENTICODE_STANDING_UNSIGNED_MIN_VERSION = (6, 3, 2)", resolver)
|
|
self.assertIn('version not in {"6.1.0", "6.1.1", "6.1.2", "6.2.0", "6.2.1", "6.3.0", "6.3.1", "6.3.2"}', resolver)
|
|
self.assertIn("not Authenticode-signed", resolver)
|
|
self.assertIn("windows_signing_backend: signpath", content)
|
|
self.assertIn('if [[ "$REQUIRE_WINDOWS_SIGNING" == "true" ]]', candidate_workflow)
|
|
self.assertIn("inputs.require_windows_signing", candidate_workflow)
|
|
self.assertIn("signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0", candidate_workflow)
|
|
self.assertIn("github-artifact-id: ${{ steps.upload-unsigned-windows.outputs.artifact-id }}", candidate_workflow)
|
|
self.assertIn("windows-signing-evidence.json", candidate_workflow)
|
|
for signpath_setting in (
|
|
"SIGNPATH_API_TOKEN",
|
|
"SIGNPATH_ORGANIZATION_ID",
|
|
"SIGNPATH_PROJECT_SLUG",
|
|
"SIGNPATH_SIGNING_POLICY_SLUG",
|
|
"SIGNPATH_ARTIFACT_CONFIGURATION_SLUG",
|
|
"SIGNPATH_EXPECTED_CERTIFICATE_SUBJECT",
|
|
):
|
|
self.assertIn(signpath_setting, candidate_workflow)
|
|
for signing_secret in (
|
|
"APPLE_DEVELOPER_ID_CERTIFICATE_P12_BASE64",
|
|
"APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD",
|
|
"APPLE_DEVELOPER_ID_APPLICATION_IDENTITY",
|
|
"APPLE_NOTARY_KEY_P8_BASE64",
|
|
"APPLE_NOTARY_KEY_ID",
|
|
"APPLE_NOTARY_ISSUER_ID",
|
|
"WINDOWS_CODE_SIGNING_CERTIFICATE_PFX_BASE64",
|
|
"WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD",
|
|
):
|
|
self.assertIn(signing_secret, candidate_workflow)
|
|
self.assertIn('tar -xzf "$tarball" -C "$extract_dir" -- "$@"', release_validator)
|
|
self.assertNotIn('tar -xOf "$tarball" "$entry"', release_validator)
|
|
self.assertIn("go run ./scripts/release_update_key.go public-key-ssh", candidate_workflow)
|
|
self.assertIn("does not trust the configured release signing key", candidate_workflow)
|
|
self.assertIn("scripts/install-mcp.sh release/install-mcp.sh", candidate_workflow)
|
|
self.assertIn("scripts/install-mcp.ps1 release/install-mcp.ps1", candidate_workflow)
|
|
self.assertIn("$PinnedReleaseSshPublicKey = '${TRUSTED_SSH_PUBLIC_KEY}'", candidate_workflow)
|
|
self.assertIn("TRUSTED_SSH_PUBLIC_KEY", update_demo_workflow)
|
|
self.assertIn("Verify signed release and select only demo server runtime", update_demo_workflow)
|
|
self.assertIn('printf \'pulse-installer %s\\n\' "$TRUSTED_SSH_PUBLIC_KEY" > /tmp/pulse-demo-signers', update_demo_workflow)
|
|
self.assertIn("ssh-keygen -Y verify -f /tmp/pulse-demo-signers -I pulse-installer", update_demo_workflow)
|
|
self.assertIn("-n pulse-install -s /tmp/pulse-demo-release.sshsig < /tmp/pulse-demo-release.tgz", update_demo_workflow)
|
|
self.assertIn("python3 .github/scripts/dispatch-demo-runtime.py prepare", update_demo_workflow)
|
|
self.assertNotIn("bash /tmp/pulse-install.sh", update_demo_workflow)
|
|
self.assertIn("bash .github/scripts/setup-demo-ssh.sh", update_demo_workflow)
|
|
self.assertIn("bash .github/scripts/check-demo-reachability.sh", update_demo_workflow)
|
|
self.assertIn("ping: ${{ secrets.DEMO_SERVER_HOST }}", update_demo_workflow)
|
|
self.assertIn("tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3", update_demo_workflow)
|
|
self.assertIn("uses: ./.github/workflows/update-demo-server.yml", deploy_demo_workflow)
|
|
self.assertIn("verify_only: true", deploy_demo_workflow)
|
|
self.assertIn('MAX_SSH_SETUP_ATTEMPTS="${DEMO_SSH_SETUP_ATTEMPTS:-3}"', demo_ssh_helper)
|
|
self.assertIn("ipaddress.ip_address(sys.argv[1])", demo_ssh_helper)
|
|
self.assertIn("host_needs_dns=false", demo_ssh_helper)
|
|
self.assertIn('getent hosts "$DEMO_SERVER_HOST"', demo_ssh_helper)
|
|
self.assertIn('ssh-keyscan -T 10 -H "$DEMO_SERVER_HOST"', demo_ssh_helper)
|
|
self.assertIn("Demo network preflight passed, but ssh-keyscan did not return host keys", demo_ssh_helper)
|
|
self.assertIn('tailscale ping --c 3 --timeout 10s "$DEMO_SERVER_HOST"', demo_reachability_helper)
|
|
self.assertIn('nc -z -w 5 "$DEMO_SERVER_HOST" "$TCP_PORT"', demo_reachability_helper)
|
|
self.assertIn("Demo peer is not present in the runner peer map yet.", demo_reachability_helper)
|
|
self.assertIn("derive the OpenSSH installer trust key from `PULSE_UPDATE_SIGNING_PUBLIC_KEY`", normalize_ws(contract))
|
|
self.assertIn('SYFT_VERSION="1.42.4"', content)
|
|
self.assertIn('SYFT_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"', content)
|
|
self.assertIn('SYFT_SHA256="590650c2743b83f327d1bf9bec64f6f83b7fec504187bb84f500c862bf8f2a0f"', content)
|
|
self.assertIn('install -m 0755 "${TMP_DIR}/syft" /usr/local/bin/syft', content)
|
|
self.assertIn('release_upload_with_retry "${TAG}" release/*.sig --clobber', content)
|
|
self.assertIn('release_upload_with_retry "${TAG}" release/*.sshsig --clobber', content)
|
|
self.assertIn('release_upload_with_retry "${TAG}" release/*.sbom.spdx.json --clobber', content)
|
|
self.assertIn('gh release upload "$@"', content)
|
|
self.assertIn('gh release upload failed on attempt ${attempt}/${max_attempts}; retrying in ${wait_seconds}s', content)
|
|
self.assertIn('gh release upload failed after ${max_attempts} attempts', content)
|
|
self.assertIn("Running current organization-sharing E2E suite...", content)
|
|
self.assertIn(
|
|
"tests/66-organization-sharing-approval-ui.spec.ts",
|
|
content,
|
|
)
|
|
self.assertNotIn("npx playwright test tests/03-multi-tenant.spec.ts", content)
|
|
self.assertIn('PULSE_E2E_ENTITLEMENT_PROFILE: "multi-tenant"', content)
|
|
self.assertIn("Collect integration diagnostics", content)
|
|
self.assertIn("release-integration-diagnostics/docker.log", content)
|
|
self.assertIn("docker ps -a || true", content)
|
|
self.assertIn("docker logs pulse-test-server", content)
|
|
self.assertIn("docker logs pulse-mock-github", content)
|
|
self.assertIn("Upload integration Playwright report", content)
|
|
self.assertIn("Upload integration failures", content)
|
|
self.assertIn("tests/integration/test-results/", content)
|
|
self.assertIn("tests/integration/release-integration-diagnostics/", content)
|
|
self.assertIn("--target runtime_prebuilt", docker_build)
|
|
self.assertIn("--target agent_runtime_prebuilt", docker_build)
|
|
self.assertIn("id-token: write", candidate_workflow)
|
|
self.assertIn("attestations: write", candidate_workflow)
|
|
self.assertIn(
|
|
"uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2",
|
|
candidate_workflow,
|
|
)
|
|
self.assertIn(
|
|
"subject-checksums: ${{ runner.temp }}/release-candidate-subjects.sha256",
|
|
candidate_workflow,
|
|
)
|
|
self.assertIn("Preserve portable build provenance", candidate_workflow)
|
|
self.assertIn("release-build-provenance.sigstore.json", candidate_workflow)
|
|
self.assertLess(
|
|
candidate_workflow.index("Validate complete candidate locally"),
|
|
candidate_workflow.index("Attest complete release candidate"),
|
|
)
|
|
self.assertLess(
|
|
candidate_workflow.index("Attest complete release candidate"),
|
|
candidate_workflow.index("Seal immutable candidate manifest"),
|
|
)
|
|
self.assertIn("release-build-provenance.sigstore.json", content)
|
|
build_script = read("scripts/build-release.sh")
|
|
release_asset_helper = read("scripts/release_asset_common.sh")
|
|
backfill_script = read("scripts/backfill-release-assets.sh")
|
|
backfill_workflow = read(".github/workflows/backfill-release-assets.yml")
|
|
self.assertIn('RELEASE_PACKET_SBOM="pulse-v${VERSION}-release.sbom.spdx.json"', build_script)
|
|
self.assertIn('SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"', build_script)
|
|
self.assertIn('PULSE_REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"', build_script)
|
|
self.assertIn('cd "${PULSE_REPO_ROOT}"', build_script)
|
|
self.assertIn('source "${SCRIPT_DIR}/release_asset_common.sh"', build_script)
|
|
self.assertIn('pulse_release_prepare_signing_state "pulse-installer" "pulse-install"', build_script)
|
|
self.assertIn('pulse_release_generate_packet_sbom "${RELEASE_DIR}" "${RELEASE_PACKET_SBOM}"', build_script)
|
|
self.assertIn('pulse_release_write_checksums_and_signatures "${RELEASE_DIR}" "${checksum_files[@]}"', build_script)
|
|
self.assertIn(': "${PULSE_SCRIPTS_DIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"', release_asset_helper)
|
|
self.assertIn(': "${PULSE_REPO_ROOT:=$(cd "${PULSE_SCRIPTS_DIR}/.." && pwd)}"', release_asset_helper)
|
|
self.assertIn('go -C "${PULSE_REPO_ROOT}" run ./scripts/release_update_key.go "$@"', release_asset_helper)
|
|
self.assertIn('pulse_release_go_run_update_key public-key --private-key "${PULSE_UPDATE_SIGNING_KEY}"', release_asset_helper)
|
|
self.assertIn(
|
|
'pulse_release_go_run_update_key fingerprint --public-key "${PULSE_RELEASE_UPDATE_PUBLIC_KEY}"',
|
|
release_asset_helper,
|
|
)
|
|
self.assertIn('pulse_release_go_run_update_key public-key-ssh --private-key "${PULSE_UPDATE_SIGNING_KEY}"', release_asset_helper)
|
|
self.assertIn('pulse_release_go_run_update_key openssh-private-key --private-key "${PULSE_UPDATE_SIGNING_KEY}"', release_asset_helper)
|
|
self.assertIn('pulse_release_go_run_update_key sign --private-key "${PULSE_UPDATE_SIGNING_KEY}" --file "${absolute_file}"', release_asset_helper)
|
|
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY", release_asset_helper)
|
|
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY_FINGERPRINT", release_asset_helper)
|
|
self.assertIn(
|
|
"Verified update signing public key fingerprint: ${PULSE_RELEASE_UPDATE_PUBLIC_KEY_FINGERPRINT}",
|
|
release_asset_helper,
|
|
)
|
|
self.assertIn('"${resolved_tool}" "dir:${release_dir}" -o "spdx-json=${tmp_sbom}"', release_asset_helper)
|
|
self.assertIn('find . -maxdepth 1 -type f \\( -name \'*.sig\' -o -name \'*.sshsig\' \\) -delete', release_asset_helper)
|
|
self.assertIn('source "${SCRIPT_DIR}/release_asset_common.sh"', backfill_script)
|
|
self.assertIn('gh release download "${TAG}" -R "${REPO}" --dir "${RELEASE_DIR}" --clobber', backfill_script)
|
|
self.assertIn('pulse_release_generate_packet_sbom "${PAYLOAD_DIR}" "${RELEASE_PACKET_SBOM}"', backfill_script)
|
|
self.assertIn('pulse_release_write_checksums_and_signatures "${RELEASE_DIR}" "${checksum_files[@]}"', backfill_script)
|
|
self.assertIn('gh release upload "${TAG}" "${RELEASE_DIR}/${RELEASE_PACKET_SBOM}" --clobber', backfill_script)
|
|
self.assertIn("name: Backfill Release Assets", backfill_workflow)
|
|
self.assertIn("workflow_dispatch:", backfill_workflow)
|
|
self.assertIn('SYFT_VERSION="1.42.4"', backfill_workflow)
|
|
self.assertIn("TAG: ${{ inputs.tag }}", backfill_workflow)
|
|
self.assertIn("REPOSITORY: ${{ github.repository }}", backfill_workflow)
|
|
self.assertIn('./scripts/backfill-release-assets.sh --tag "${TAG}" --repo "${REPOSITORY}"', backfill_workflow)
|
|
self.assertIn('./scripts/validate-published-release.sh "${TAG}" "${REPOSITORY}"', backfill_workflow)
|
|
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}", backfill_workflow)
|
|
self.assertIn(
|
|
normalize_ws(
|
|
"""
|
|
- name: Validate published release packet
|
|
env:
|
|
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
|
REPOSITORY: ${{ github.repository }}
|
|
TAG: ${{ inputs.tag }}
|
|
run: |
|
|
./scripts/validate-published-release.sh "${TAG}" "${REPOSITORY}"
|
|
"""
|
|
),
|
|
normalize_ws(backfill_workflow),
|
|
)
|
|
self.assertIn("./scripts/prepare-release-container-context.sh", candidate_workflow)
|
|
self.assertIn('test "${actual_server}" = "${expected_server}"', docker_build)
|
|
self.assertIn('test "${actual_agent}" = "${expected_agent}"', docker_build)
|
|
self.assertNotIn("secrets.", docker_build)
|
|
self.assertNotIn("provenance: false", content)
|
|
self.assertIn("Derived rollback command:", helper)
|
|
self.assertIn("sudo /bin/update --version", helper)
|
|
self.assertIn("v6 GA date to publish with GA", helper)
|
|
self.assertIn("--arg ga_date \"$GA_DATE\"", helper)
|
|
self.assertIn("ga_date", resolver)
|
|
self.assertIn("v5_eos_date", resolver)
|
|
self.assertIn("Stable v6.0.0 requires v5_eos_date in YYYY-MM-DD form", resolver)
|
|
self.assertIn("release_notes must include the Pulse v5 maintenance-only support notice", resolver)
|
|
dry_run_workflow = read(".github/workflows/release-dry-run.yml")
|
|
self.assertIn("Required rollback stable version to rehearse", dry_run_workflow)
|
|
self.assertIn("rollback_version:\n description: 'Required rollback stable version to rehearse", dry_run_workflow)
|
|
# The fixed watchdog has no rollback override, so its dispatch schema
|
|
# must permit omission. Ordinary manual rehearsals still require an
|
|
# explicit rollback in the pre-checkout admission shell.
|
|
dry_run = yaml.safe_load(dry_run_workflow)
|
|
inputs = dry_run.get("on", dry_run.get(True))["workflow_dispatch"]["inputs"]
|
|
self.assertIs(inputs["rollback_version"]["required"], False)
|
|
self.assertIn('workflow_dispatch:false)', dry_run_workflow)
|
|
self.assertIn('[ -z "${ROLLBACK_VERSION_INPUT:-}" ]', dry_run_workflow)
|
|
self.assertIn('Candidate rehearsal requires explicit rollback and no watchdog SHA.', dry_run_workflow)
|
|
# Both admitted watchdog modes observe the preceding stable reference,
|
|
# without manufacturing a new promotion envelope for an already-shipped
|
|
# VERSION. Candidate rehearsals still use the complete resolver.
|
|
self.assertIn(
|
|
'if [ "${WATCHDOG_MODE}" = "true" ]; then',
|
|
dry_run_workflow,
|
|
)
|
|
self.assertIn("derive_latest_stable_rollback_tag(version, list_stable_tags())", dry_run_workflow)
|
|
self.assertNotIn("--derive-rollback-latest-stable", dry_run_workflow)
|
|
self.assertIn("--derive-rollback-latest-stable", resolver)
|
|
self.assertIn("derive_latest_stable_rollback_tag", resolver)
|
|
self.assertIn("Required: prior stable version to pin for rollback", content)
|
|
self.assertIn("rollback_version:\n description: 'Required: prior stable version to pin for rollback", content)
|
|
self.assertIn("check-workflow-dispatch-inputs.py", helper)
|
|
self.assertIn('--branch "$CURRENT_BRANCH"', helper)
|
|
self.assertIn('--ref "$CURRENT_BRANCH"', helper)
|
|
self.assertIn("Release automation executes the selected remote ref", helper)
|
|
self.assertNotIn("Continue anyway?", helper)
|
|
self.assertIn("Audit header composition", content)
|
|
self.assertIn("run: npm --prefix frontend-modern run lint:headers", content)
|
|
self.assertIn("pushed governed release-branch copy of `.github/workflows/release-dry-run.yml`", policy)
|
|
self.assertIn("GitHub executes the selected remote ref", normalize_ws(policy))
|
|
checklist = read("docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md")
|
|
self.assertIn("pushed governed release-branch copy of `.github/workflows/release-dry-run.yml`", checklist)
|
|
self.assertIn("workflow_dispatch", checklist)
|
|
self.assertIn("selected remote ref", normalize_ws(checklist))
|
|
self.assertIn("local rehearsal branch exactly matches `origin`", checklist)
|
|
self.assertIn("derive the governed release branch from release-control metadata", checklist)
|
|
template = read("docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md")
|
|
self.assertIn("governed release line from `control_plane.json`", template)
|
|
self.assertIn("pulse/v6-release", template)
|
|
self.assertIn("record_rc_to_ga_rehearsal.py --run-id <run-id>", runbook)
|
|
self.assertIn("rc-to-ga-promotion-readiness-rehearsal-<record-date>.md", runbook)
|
|
self.assertIn("Existing unpublished draft releases for the same tag are updated in place", runbook)
|
|
self.assertIn("Do not rewrite shipped RC notes in place", runbook)
|
|
self.assertIn("`rc.1`, `rc.2`, and later prerelease", runbook)
|
|
self.assertIn("The current RC release packet is prepared and internally linked", runbook)
|
|
self.assertIn("operators know the update signer changed", normalize_ws(runbook))
|
|
self.assertIn("manual reinstall or other explicit trust-migration path", normalize_ws(runbook))
|
|
self.assertIn("points at the current in-repo draft packet", runbook)
|
|
# Runbook example version tracks the current VERSION file (any active 6.0.0-rc.N).
|
|
current_version = read("VERSION").strip()
|
|
if rc_packet_paths_for_version(current_version) is not None:
|
|
self.assertIn(f'export RC_VERSION="{current_version}"', runbook)
|
|
self.assertIn("printf '%s\\n' \"$RC_VERSION\" > VERSION", runbook)
|
|
self.assertIn("canonical file-backed helper", runbook)
|
|
self.assertIn(
|
|
'./scripts/trigger-release.sh "$RC_VERSION" "$RELEASE_NOTES_FILE"',
|
|
runbook,
|
|
)
|
|
self.assertIn("Do not paste multiline release notes", runbook)
|
|
self.assertIn("compares GitHub's stored body byte-for-byte", runbook)
|
|
self.assertIn("Keep the current release-notes, changelog, and operator-support packet in", runbook)
|
|
self.assertIn("Published release bodies must also stay publication-safe", contract)
|
|
self.assertIn("Release-note transport is file-backed and fail-closed", contract)
|
|
self.assertIn("malformed edited body is quarantined", contract)
|
|
self.assertIn("must state the continuity impact explicitly", normalize_ws(contract))
|
|
self.assertIn(
|
|
"append the standardized installation and promotion metadata sections exactly once",
|
|
normalize_ws(contract),
|
|
)
|
|
|
|
|
|
def test_create_release_locates_its_release_through_a_bounded_complete_listing_retry(self) -> None:
|
|
"""Run the real step script against doubles: GitHub 504s must be retried, never read as absence."""
|
|
workflow = yaml.safe_load(read(".github/workflows/create-release.yml"))
|
|
step = next(s for s in workflow["jobs"]["create_release"]["steps"]
|
|
if s.get("name") == "Locate existing release")
|
|
script = step["run"].replace("${{ github.repository }}", "owner/repo")
|
|
pages = (
|
|
'[{"id": 1, "tag_name": "v6.5.0", "draft": false, "assets": []}]\n'
|
|
'[{"id": 42, "tag_name": "v6.6.0-beta.1", "draft": true, "html_url": "https://example.test/42",'
|
|
' "target_commitish": "abc", "published_at": null, "assets": [{"name": "checksums.txt"}]}]\n'
|
|
)
|
|
fake_gh = (
|
|
'#!/bin/bash\n'
|
|
'n=$(cat "$FIX/count" 2>/dev/null || echo 0); n=$((n + 1)); echo "$n" > "$FIX/count"\n'
|
|
'case "$MODE" in\n'
|
|
' flaky) if [ "$n" -le 2 ]; then\n'
|
|
' echo "gh: We could not respond to your request in time (HTTP 504)" >&2; exit 1; fi\n'
|
|
' cat "$FIX/pages.json" ;;\n'
|
|
' truncated) printf \'[{"id": 42, "tag_name": \' ; exit 0 ;;\n'
|
|
' ok) cat "$FIX/pages.json" ;;\n'
|
|
'esac\n'
|
|
)
|
|
fake_sleep = '#!/bin/bash\necho "$1" >> "$FIX/sleeps"\n'
|
|
|
|
def outputs(text: str) -> dict:
|
|
# write_github_output.py writes name<<delimiter, value, delimiter blocks.
|
|
return {m.group(1): m.group(3)
|
|
for m in re.finditer(r"^(\w+)<<(\S+)\n(.*?)\n\2$", text, re.MULTILINE | re.DOTALL)}
|
|
|
|
def run(mode: str, tag: str) -> dict:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
fix = Path(directory)
|
|
(fix / "bin").mkdir()
|
|
for name, body in (("gh", fake_gh), ("sleep", fake_sleep)):
|
|
(fix / "bin" / name).write_text(body)
|
|
(fix / "bin" / name).chmod(0o755)
|
|
(fix / "pages.json").write_text(pages)
|
|
output = fix / "github_output"
|
|
output.write_text("")
|
|
env = {**os.environ, "FIX": str(fix), "MODE": mode, "WORKFLOW_OUTPUT_1": tag,
|
|
"GH_TOKEN": "unused", "GITHUB_OUTPUT": str(output),
|
|
"PATH": f"{fix / 'bin'}{os.pathsep}{os.environ['PATH']}"}
|
|
result = subprocess.run(["bash", "--noprofile", "--norc", "-eo", "pipefail", "-c", script],
|
|
cwd=REPO_ROOT, env=env, text=True, capture_output=True, timeout=60)
|
|
read_text = lambda name: (fix / name).read_text() if (fix / name).exists() else ""
|
|
return {"result": result, "output": output.read_text(),
|
|
"calls": int(read_text("count").strip() or 0),
|
|
"sleeps": read_text("sleeps").split()}
|
|
|
|
# Two GitHub 504s, then a complete listing: retried with backoff, then the draft is found.
|
|
flaky = run("flaky", "v6.6.0-beta.1")
|
|
self.assertEqual(flaky["result"].returncode, 0, flaky["result"].stderr)
|
|
self.assertEqual((flaky["calls"], flaky["sleeps"]), (3, ["15", "30"]))
|
|
self.assertEqual(outputs(flaky["output"]).get("release_id"), "42")
|
|
self.assertEqual(outputs(flaky["output"]).get("release_is_draft"), "true")
|
|
|
|
# A read that exits 0 but is truncated every time is never an empty answer: five attempts,
|
|
# capped backoff, an explicit error, and no outputs written.
|
|
truncated = run("truncated", "v6.6.0-beta.1")
|
|
self.assertNotEqual(truncated["result"].returncode, 0)
|
|
self.assertIn("releases listing failed after 5 attempts", truncated["result"].stdout)
|
|
self.assertEqual((truncated["calls"], truncated["sleeps"]), (5, ["15", "30", "60", "120"]))
|
|
self.assertEqual(truncated["output"], "")
|
|
|
|
# A complete listing that does not contain the tag still means "no existing release".
|
|
absent = run("ok", "v6.7.0")
|
|
self.assertEqual(absent["result"].returncode, 0, absent["result"].stderr)
|
|
self.assertEqual((absent["calls"], absent["sleeps"]), (1, []))
|
|
self.assertEqual(outputs(absent["output"]).get("release_id"), "")
|
|
|
|
def test_v620_owner_soak_waiver_is_version_bound(self) -> None:
|
|
policy = normalize_ws(
|
|
read("docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md")
|
|
)
|
|
owner_record = normalize_ws(
|
|
read(
|
|
"docs/release-control/v6/internal/records/"
|
|
"v6.2.0-stable-cutoff-owner-approval-2026-08-09.md"
|
|
)
|
|
)
|
|
|
|
self.assertIn("v6.2.0 release-cutoff exception", policy)
|
|
self.assertIn("not soak evidence and not a standing exception", policy)
|
|
self.assertIn("`v6.2.0`-only unsigned-Windows exception", policy)
|
|
self.assertIn("not a standing decision for later releases", policy)
|
|
self.assertIn("Promoted prerelease: `v6.2.0-rc.11`", owner_record)
|
|
self.assertIn("Rollback target: `v6.1.2`", owner_record)
|
|
self.assertIn(
|
|
"Exact rollback reinstall command: `./scripts/install.sh --version v6.1.2`",
|
|
owner_record,
|
|
)
|
|
self.assertIn("explicitly approved a `v6.2.0`-only unsigned-Windows exception", owner_record)
|
|
self.assertIn("not Authenticode-signed", owner_record)
|
|
self.assertIn("Unknown Publisher warning", owner_record)
|
|
|
|
def test_v621_unsigned_windows_owner_exception_is_version_bound(self) -> None:
|
|
policy = normalize_ws(
|
|
read("docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md")
|
|
)
|
|
owner_record = normalize_ws(
|
|
read(
|
|
"docs/release-control/v6/internal/records/"
|
|
"v6.2.1-unsigned-windows-owner-approval-2026-08-10.md"
|
|
)
|
|
)
|
|
|
|
self.assertIn("v6.2.1 unsigned-Windows exception", policy)
|
|
self.assertIn("release run `31343128024` failed closed", policy)
|
|
self.assertIn("explicitly authorized unsigned Windows artifacts", policy)
|
|
self.assertIn("Stable `v6.2.2` and later restore mandatory Authenticode", policy)
|
|
self.assertIn("Release: `v6.2.1`", owner_record)
|
|
self.assertIn("Failed release run: `31343128024`", owner_record)
|
|
self.assertIn("explicitly authorized unsigned Windows Unified Agent artifacts", owner_record)
|
|
self.assertIn("not Authenticode-signed", owner_record)
|
|
self.assertIn("Unknown Publisher warning", owner_record)
|
|
|
|
def test_v630_owner_telemetry_cutoff_is_version_bound(self) -> None:
|
|
policy = normalize_ws(
|
|
read("docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md")
|
|
)
|
|
owner_record = normalize_ws(
|
|
read(
|
|
"docs/release-control/v6/internal/records/"
|
|
"v6.3.0-stable-cutoff-owner-approval-2026-08-22.md"
|
|
)
|
|
)
|
|
|
|
self.assertIn("v6.3.0 release-cutoff exception", policy)
|
|
self.assertIn("bounded v6.3.0 owner-risk acceptance", policy)
|
|
self.assertIn("not soak evidence and not a standing exception", policy)
|
|
self.assertIn("Promoted prerelease: `v6.3.0-rc.6`", owner_record)
|
|
self.assertIn("Rollback target: `v6.2.1`", owner_record)
|
|
self.assertIn(
|
|
"Exact rollback reinstall command: `./scripts/install.sh --version v6.2.1`",
|
|
owner_record,
|
|
)
|
|
self.assertIn("18 active installs: 10 binary and 8 Docker", owner_record)
|
|
self.assertIn("56 rolling-window update attempts, 56 successes, and zero failures", owner_record)
|
|
self.assertIn("separate, version-bound `v6.3.0` unsigned-Windows decision", owner_record)
|
|
|
|
signing_record = normalize_ws(
|
|
read(
|
|
"docs/release-control/v6/internal/records/"
|
|
"v6.3.0-unsigned-windows-owner-approval-2026-08-22.md"
|
|
)
|
|
)
|
|
self.assertIn("v6.3.0 unsigned-Windows exception", policy)
|
|
self.assertIn("Stable `v6.3.1` and later restore mandatory Authenticode", policy)
|
|
self.assertIn("Windows Authenticode signing is not yet available", signing_record)
|
|
self.assertIn("not Authenticode-signed", signing_record)
|
|
self.assertIn("Unknown Publisher warning", signing_record)
|
|
|
|
patch_signing_record = normalize_ws(
|
|
read(
|
|
"docs/release-control/v6/internal/records/"
|
|
"v6.3.1-unsigned-windows-owner-approval-2026-08-23.md"
|
|
)
|
|
)
|
|
self.assertIn("v6.3.1 unsigned-Windows exception", policy)
|
|
self.assertIn("Stable `v6.3.2` and later restore mandatory Authenticode", policy)
|
|
self.assertIn("SignPath production certificate remains `CSR PENDING`", patch_signing_record)
|
|
self.assertIn("not Authenticode-signed", patch_signing_record)
|
|
self.assertIn("Unknown Publisher warning", patch_signing_record)
|
|
|
|
def test_v640_expedited_stable_cutoff_is_version_bound(self) -> None:
|
|
policy = normalize_ws(
|
|
read("docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md")
|
|
)
|
|
owner_record = normalize_ws(
|
|
read(
|
|
"docs/release-control/v6/internal/records/"
|
|
"v6.4.0-stable-cutoff-owner-approval-2026-08-28.md"
|
|
)
|
|
)
|
|
|
|
self.assertIn("v6.4.0 expedited stable-cutoff exception", policy)
|
|
self.assertIn("not soak evidence and not a standing exception", policy)
|
|
self.assertIn("Runtime content cutoff: `18b22d1ebbfe542484652e419320fc7643a792f0`", owner_record)
|
|
self.assertIn("Promoted published prerelease: `v6.4.0-rc.12`", owner_record)
|
|
self.assertIn("Rollback target: `v6.3.2`", owner_record)
|
|
self.assertIn(
|
|
"Exact rollback reinstall command: `./scripts/install.sh --version v6.3.2`",
|
|
owner_record,
|
|
)
|
|
self.assertIn("active customer harm", owner_record)
|
|
self.assertIn("not 72-hour soak evidence", owner_record)
|
|
self.assertIn("standing Windows Authenticode-unavailable policy", owner_record)
|
|
self.assertIn("not Authenticode-signed", owner_record)
|
|
self.assertIn("Unknown Publisher warning", owner_record)
|
|
|
|
unavailable_signing_record = normalize_ws(
|
|
read(
|
|
"docs/release-control/v6/internal/records/"
|
|
"windows-authenticode-unavailable-owner-policy-2026-08-25.md"
|
|
)
|
|
)
|
|
self.assertIn("Windows Authenticode unavailable policy from v6.3.2", policy)
|
|
self.assertIn("must not require per-version unsigned allowlist updates", policy)
|
|
self.assertIn("Invalid request to SignPath API", unavailable_signing_record)
|
|
self.assertIn("not Authenticode-signed", unavailable_signing_record)
|
|
self.assertIn("Unknown Publisher warning", unavailable_signing_record)
|
|
|
|
def test_release_artifact_workflows_refuse_stable_without_matching_rc(self) -> None:
|
|
publish = read(".github/workflows/publish-docker.yml")
|
|
promote = read(".github/workflows/promote-floating-tags.yml")
|
|
demo = read(".github/workflows/update-demo-server.yml")
|
|
demo_profile = read(".github/scripts/resolve-demo-runtime-profile.sh")
|
|
preview_deploy = read(".github/workflows/deploy-demo-server.yml")
|
|
release_workflow = read(".github/workflows/create-release.yml")
|
|
candidate_workflow = read(".github/workflows/build-release-candidate.yml")
|
|
qualifier_workflow = read(".github/workflows/qualify-release-containers.yml")
|
|
dry_run_workflow = read(".github/workflows/release-dry-run.yml")
|
|
helm = read(".github/workflows/publish-helm-chart.yml")
|
|
helm_pages = read(".github/workflows/helm-pages.yml")
|
|
convergence = read(".github/workflows/release-convergence.yml")
|
|
artifact_validator = read("scripts/release_control/validate_artifact_release_line.py")
|
|
chart = read("deploy/helm/pulse/Chart.yaml")
|
|
chart_sync = read("scripts/sync_chart_release_metadata.py")
|
|
demo_smoke = read("scripts/demo_public_browser_smoke.cjs")
|
|
runbook = read("docs/releases/V6_PRERELEASE_RUNBOOK.md")
|
|
self.assertIn("validate_artifact_release_line.py", publish)
|
|
self.assertIn('--anticipated-source-sha "${SOURCE_SHA}"', publish)
|
|
self.assertIn('test "$(git rev-parse HEAD)" = "${EXPECTED_SOURCE_SHA}"', publish)
|
|
self.assertIn("if tag_exists_fn(normalized_tag):", artifact_validator)
|
|
self.assertIn("not anticipated source", artifact_validator)
|
|
self.assertIn("provenance: mode=max", publish)
|
|
self.assertIn("sbom: true", publish)
|
|
self.assertIn("name: Publish ${{ matrix.image }} image", publish)
|
|
self.assertIn("fail-fast: false", publish)
|
|
self.assertIn("if: matrix.image == 'server'", publish)
|
|
self.assertIn("if: matrix.image == 'control-plane'", publish)
|
|
self.assertIn("uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2", publish)
|
|
self.assertIn("subject-name: docker.io/rcourtman/pulse", publish)
|
|
self.assertIn("subject-name: ghcr.io/${{ github.repository_owner }}/pulse", publish)
|
|
# pulse-agent ships as release-asset binaries, not as a Docker image
|
|
# (see commit dropping the agent image publish steps). The agent
|
|
# attestation subject-names intentionally do not appear here.
|
|
self.assertNotIn("subject-name: docker.io/rcourtman/pulse-agent", publish)
|
|
self.assertNotIn("subject-name: ghcr.io/${{ github.repository_owner }}/pulse-agent", publish)
|
|
self.assertIn("create-storage-record: false", publish)
|
|
self.assertIn("server_digest:", publish)
|
|
self.assertIn("control_plane_digest:", publish)
|
|
self.assertIn("Verify exact image identities and provenance", publish)
|
|
self.assertIn("verify-release-container-images.sh", publish)
|
|
self.assertIn("target: runtime_prebuilt", publish)
|
|
self.assertIn("target: control_plane_prebuilt", publish)
|
|
self.assertIn("Verify exact-candidate container payload", publish)
|
|
self.assertNotIn("PULSE_LICENSE_PUBLIC_KEY", publish)
|
|
self.assertNotIn("PULSE_UPDATE_SIGNING_KEY", publish)
|
|
self.assertNotIn("provenance: false", publish)
|
|
self.assertIn("validate_artifact_release_line.py", promote)
|
|
self.assertIn("source_sha:", promote)
|
|
self.assertIn("server_digest:", promote)
|
|
self.assertIn("control_plane_digest:", promote)
|
|
self.assertIn("verify-release-container-images.sh", promote)
|
|
self.assertIn('docker_source="docker.io/rcourtman/${image}@${expected_digest}"', promote)
|
|
self.assertIn('ghcr_source="ghcr.io/${OWNER}/${image}@${expected_digest}"', promote)
|
|
self.assertNotIn('"rcourtman/${image}:${TAG}"', promote)
|
|
self.assertNotIn('"ghcr.io/${OWNER}/${image}:${TAG}"', promote)
|
|
self.assertIn("control_plane.py --branch-for-version", demo)
|
|
self.assertIn("demo-stable", demo)
|
|
self.assertIn("Refusing prerelease tag", demo)
|
|
self.assertIn("Prerelease demo updates are retired after v6 GA", demo)
|
|
self.assertIn("The latest alias is allowed only for verification-only checks.", demo)
|
|
self.assertIn("Resolved verification-only target to latest stable release", demo)
|
|
self.assertNotIn("github.event_name == 'release'", demo)
|
|
self.assertNotIn("preview-v6", demo)
|
|
self.assertNotIn("demo-preview-v6", demo)
|
|
self.assertNotIn('SERVICE_NAME="pulse-v6-preview"', demo)
|
|
self.assertNotIn("Preview demo updates must not target the stable pulse service.", demo)
|
|
self.assertIn("workflow_call:", demo)
|
|
self.assertIn("verify_only:", demo)
|
|
self.assertIn("tag: latest", dry_run_workflow)
|
|
self.assertIn("verify_only: true", dry_run_workflow)
|
|
self.assertIn("Verify Current Stable Demo Path (No Mutation)", dry_run_workflow)
|
|
self.assertIn("tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3", demo)
|
|
self.assertIn("oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}", demo)
|
|
self.assertIn("oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}", demo)
|
|
self.assertIn("ping: ${{ secrets.DEMO_SERVER_HOST }}", demo)
|
|
# The static 90-day TS_AUTHKEY was retired for the OAuth client
|
|
# (0a9a29d63); the runner mints an ephemeral tagged node key per run.
|
|
self.assertNotIn("TS_AUTHKEY", demo)
|
|
self.assertIn("DEMO_EXPECTED_HOSTNAME", demo)
|
|
self.assertIn("Verify target host identity", demo)
|
|
self.assertIn("Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.", demo)
|
|
self.assertIn("Apply guarded demo transaction", demo)
|
|
self.assertIn("dispatch-demo-runtime.py", demo)
|
|
self.assertIn("Retain guarded demo transaction evidence", demo)
|
|
self.assertIn("Resolve target-compatible demo runtime profile", demo)
|
|
self.assertIn("mockEagerHistoryPVEGuestLimit", demo_profile)
|
|
self.assertIn("UpdateMetricCohort", demo_profile)
|
|
self.assertIn("mockLargeEstateStartupReady", demo_profile)
|
|
self.assertIn('PROFILE="legacy-bounded"', demo_profile)
|
|
self.assertIn("MOCK_NODES=8", demo_profile)
|
|
self.assertIn("MOCK_VMS_PER_NODE=6", demo_profile)
|
|
self.assertIn("MOCK_LXCS_PER_NODE=4", demo_profile)
|
|
self.assertIn("MOCK_DOCKER_HOSTS=2", demo_profile)
|
|
self.assertIn("MOCK_DOCKER_CONTAINERS=8", demo_profile)
|
|
self.assertIn("MOCK_GENERIC_HOSTS=2", demo_profile)
|
|
self.assertIn("MOCK_K8S_CLUSTERS=1", demo_profile)
|
|
self.assertIn("MOCK_K8S_NODES=3", demo_profile)
|
|
self.assertIn("MOCK_K8S_PODS=12", demo_profile)
|
|
self.assertIn("MOCK_K8S_DEPLOYMENTS=4", demo_profile)
|
|
self.assertIn("MOCK_SEED_DURATION=2h", demo_profile)
|
|
self.assertIn("MOCK_SAMPLE_INTERVAL=5m", demo_profile)
|
|
self.assertIn("MOCK_UPDATE_INTERVAL=15s", demo_profile)
|
|
# The owned transaction adjusts only the demo runtime profile. It
|
|
# requires an existing entitlement; it must not grant one by rewriting
|
|
# signed billing state or clearing its integrity check.
|
|
transaction = read(".github/scripts/demo-runtime-transaction.py")
|
|
for required in ('"DEMO_MODE": "true"', '"PULSE_MOCK_MODE": "true"',
|
|
'"PULSE_MOCK_SEED_METRICS_STORE": "false"',
|
|
'"seed_duration": "trends_seed_duration"', '"sample_interval": "trends_sample_interval"',
|
|
'"PULSE_MOCK_" + key.upper()', 'self.profile_admission()',
|
|
'if "demo_fixtures" not in billing["capabilities"]:',
|
|
'raise Failure("billing-demo-capability-required")',
|
|
'os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK',
|
|
'self.host.profile_admission()',
|
|
'self.host.profile(self.request, not healthy_baseline)', 'self.host.start()'):
|
|
self.assertIn(required, transaction)
|
|
self.assertNotIn('billing.pop("integrity", None)', transaction)
|
|
self.assertNotIn('billing["capabilities"] = ["demo_fixtures"]', transaction)
|
|
self.assertNotIn('atomic_json(billing_file, billing)', transaction)
|
|
self.assertNotIn('billing_file.write_text(', transaction)
|
|
self.assertNotIn('billing_file.write_bytes(', transaction)
|
|
self.assertIn("MOCK_NODES: ${{ needs.resolve.outputs.mock_nodes }}", demo)
|
|
self.assertIn("MOCK_UPDATE_INTERVAL: ${{ needs.resolve.outputs.mock_update_interval }}", demo)
|
|
self.assertIn("/api/license/runtime-capabilities", demo)
|
|
self.assertIn("Mock mode enabled", demo)
|
|
self.assertIn("Demo server mock mode did not enable after entitlement sync", demo)
|
|
self.assertIn(".resources | if type == \"array\" then length else 0 end", demo)
|
|
self.assertNotIn(".nodes | length", demo)
|
|
self.assertIn("Mock resources detected", demo)
|
|
self.assertIn("canonical mock resources are missing", demo)
|
|
self.assertIn("Verify frontend parity", demo)
|
|
self.assertIn("Verify public browser smoke", demo)
|
|
self.assertIn("./scripts/run_demo_public_browser_smoke.sh", demo)
|
|
self.assertIn("extract_entry_asset()", demo)
|
|
self.assertIn(r'<script\b[^>]*\bsrc=\"(/assets/index-[^\"]*\.js)\"', demo)
|
|
self.assertIn("Public demo is serving $PUBLIC_ASSET but the target service is serving $REMOTE_ASSET.", demo)
|
|
self.assertIn("uses: ./.github/workflows/publish-docker.yml", release_workflow)
|
|
self.assertIn("release-convergence.yml/dispatches", release_workflow)
|
|
self.assertIn("uses: ./.github/workflows/build-release-candidate.yml", release_workflow)
|
|
self.assertIn("Build Immutable Release Candidate", release_workflow)
|
|
self.assertIn("Release Activation Commit Verdict", release_workflow)
|
|
self.assertNotIn("Require recent exact-SHA stable patch preflight", release_workflow)
|
|
self.assertNotIn("gh workflow run update-demo-server.yml", release_workflow)
|
|
self.assertNotIn("gh workflow run publish-docker.yml", release_workflow)
|
|
self.assertIn("Verify Current Committed Stable Demo (No Mutation)", preview_deploy)
|
|
self.assertIn("uses: ./.github/workflows/update-demo-server.yml", preview_deploy)
|
|
self.assertIn("tag: latest", preview_deploy)
|
|
self.assertIn("verify_only: true", preview_deploy)
|
|
self.assertNotIn("go build", preview_deploy)
|
|
self.assertNotIn("scp ", preview_deploy)
|
|
self.assertIn("validate_artifact_release_line.py", helm)
|
|
self.assertIn(".github/workflows/create-release.yml", helm_pages)
|
|
self.assertIn("release-activation.json", helm_pages)
|
|
self.assertNotIn('gh run download "${SOURCE_RELEASE_RUN_ID}"', helm_pages)
|
|
self.assertIn('"${GITHUB_REPOSITORY}" "${CHART_DIGEST}" "${CHART_PATH}"', helm_pages)
|
|
self.assertIn("release_branch_for_version", artifact_validator)
|
|
self.assertIn("matching prerelease tag", artifact_validator)
|
|
self.assertIn("previous stable tag", artifact_validator)
|
|
self.assertIn("stable_patch", artifact_validator)
|
|
self.assertIn("Refusing {purpose}", artifact_validator)
|
|
self.assertIn("Assemble exact-candidate runtime and agent images", qualifier_workflow)
|
|
self.assertIn('kind load docker-image "${SMOKE_IMAGE_REPOSITORY}:${SMOKE_IMAGE_TAG}" --name pulse-test', qualifier_workflow)
|
|
self.assertIn('--set image.repository="${SMOKE_IMAGE_REPOSITORY}"', qualifier_workflow)
|
|
|
|
self.assertIn('--set image.pullPolicy=Never', qualifier_workflow)
|
|
self.assertNotIn("needs.docker_build.result", release_workflow)
|
|
self.assertNotIn("needs.helm_smoke.result", release_workflow)
|
|
self.assertIn('qualified chart metadata does not match the activated release', helm_pages)
|
|
self.assertIn("workflow_call:", helm_pages)
|
|
self.assertNotIn("workflow_run:", helm_pages)
|
|
self.assertNotIn('gh run download "${SOURCE_RELEASE_RUN_ID}"', helm_pages)
|
|
self.assertIn('"${GITHUB_REPOSITORY}" "${CHART_DIGEST}" "${CHART_PATH}"', helm_pages)
|
|
self.assertIn("release-activation.json", helm_pages)
|
|
self.assertIn(".github/workflows/create-release.yml", helm_pages)
|
|
self.assertNotIn('git pull --rebase origin "$REQUIRED_BRANCH"', helm_pages)
|
|
self.assertNotIn('git push origin HEAD:"$REQUIRED_BRANCH"', helm_pages)
|
|
self.assertNotIn("HELM_DOCS_VERSION", helm_pages)
|
|
self.assertNotIn("helm package deploy/helm/pulse", helm_pages)
|
|
self.assertNotIn("git pull --rebase origin main", helm_pages)
|
|
self.assertNotIn("git push origin main", helm_pages)
|
|
self.assertNotIn("kind load docker-image", helm_pages)
|
|
self.assertIn("Publish chart release and merge Pages index", helm_pages)
|
|
self.assertIn('gh release create "${chart_release}" "${chart_path}"', helm_pages)
|
|
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 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)
|
|
self.assertIn("Release Activation Commit Verdict", release_workflow)
|
|
self.assertNotIn('gh workflow run update-demo-server.yml --ref "${REQUIRED_BRANCH}"', release_workflow)
|
|
self.assertNotIn('TARGET="preview-v6"', release_workflow)
|
|
self.assertIn("sync_chart_release_metadata.py", helm)
|
|
self.assertNotIn("sync_chart_release_metadata.py", helm_pages)
|
|
self.assertIn("--chart deploy/helm/pulse/Chart.yaml", helm)
|
|
version_guard = 'if [ "$APP_VERSION" != "$CHART_VERSION" ]; then'
|
|
self.assertIn(version_guard, helm)
|
|
self.assertLess(helm.index(version_guard), helm.index(" - name: Package chart"))
|
|
self.assertIn(
|
|
"python3 scripts/release_control/helm_publish_version_test.py",
|
|
(Path(__file__).resolve().parents[2] / ".github/workflows/canonical-governance.yml").read_text(),
|
|
)
|
|
self.assertIn('git checkout --detach "refs/tags/${RELEASE_TAG}"', helm)
|
|
self.assertIn("Verify public GHCR chart identity and provenance", helm)
|
|
self.assertIn("helm registry logout ghcr.io || true", helm)
|
|
self.assertIn("actions/attest@", helm)
|
|
self.assertIn("verify-release-helm-chart.sh", helm)
|
|
self.assertIn("subject-digest: ${{ steps.push.outputs.chart_digest }}", helm)
|
|
self.assertIn("value: ${{ jobs.publish.outputs.chart_digest }}", helm)
|
|
self.assertIn("chart_digest: ${{ steps.proof.outputs.chart_digest }}", helm)
|
|
self.assertIn("${GITHUB_SHA} does not match ${RELEASE_TAG}", helm)
|
|
self.assertIn("verify-release-helm-chart.sh", helm_pages)
|
|
self.assertRegex(
|
|
helm_pages,
|
|
r"(?s)- name: Recover immutable qualified chart\n\s+env:\n\s+GH_TOKEN: \$\{\{ github\.token \}\}",
|
|
)
|
|
self.assertIn(
|
|
"chart_digest: ${{ needs.acquire_customer_promotion_lease.outputs.helm_chart_digest }}",
|
|
convergence,
|
|
)
|
|
self.assertNotIn("versions/latest/restore", helm)
|
|
self.assertNotIn("-f visibility=public", helm)
|
|
self.assertNotIn("Package visibility configuration attempted", helm)
|
|
self.assertNotIn("blob/main/docs/KUBERNETES.md", chart)
|
|
self.assertNotIn("raw.githubusercontent.com/rcourtman/Pulse/main/docs/images/pulse-logo.svg", chart)
|
|
self.assertIn("blob/{tag}/docs/KUBERNETES.md", chart_sync)
|
|
self.assertIn("raw.githubusercontent.com/{repo}/{tag}/docs/images/pulse-logo.svg", chart_sync)
|
|
self.assertIn("both stable and prerelease releases dispatch", runbook)
|
|
self.assertIn("Release `6.0.0` from `pulse/v6-release`", runbook)
|
|
self.assertIn("Prerelease public demo deployment is retired after v6 GA", runbook)
|
|
self.assertNotIn("separate v6 preview demo environment", runbook)
|
|
self.assertNotIn("preview-v6", runbook)
|
|
self.assertIn(promotion_metadata_envelope(), normalize_ws(runbook))
|
|
self.assertIn("waitUntil: 'domcontentloaded'", demo_smoke)
|
|
self.assertIn("getByLabel('Username').waitFor({ state: 'visible', timeout: 120000 })", demo_smoke)
|
|
self.assertIn("getByLabel('Password').waitFor({ state: 'visible', timeout: 120000 })", demo_smoke)
|
|
self.assertIn("getByRole('button', { name: 'Sign in to Pulse' }).waitFor({ state: 'visible', timeout: 120000 })", demo_smoke)
|
|
self.assertIn("PULSE_DEMO_AUTH_USER", demo_smoke)
|
|
self.assertIn("PULSE_DEMO_AUTH_PASS", demo_smoke)
|
|
self.assertIn("getByRole('button', { name: 'Sign in to Pulse' }).click()", demo_smoke)
|
|
self.assertIn("page.locator('main').waitFor({ state: 'visible', timeout: 60000 })", demo_smoke)
|
|
self.assertIn("getByRole('status', { name: 'Backend and live data stream are connected.' })", demo_smoke)
|
|
self.assertNotIn("getByText('Connected', { exact: true })", demo_smoke)
|
|
self.assertIn("Public demo remained on the authenticated loading shell", demo_smoke)
|
|
self.assertNotIn("waitUntil: 'networkidle'", demo_smoke)
|
|
|
|
def test_stable_demo_recovery_is_fixed_current_bits_only(self) -> None:
|
|
workflow = read(".github/workflows/recover-demo-server.yml")
|
|
recovery = read(".github/scripts/recover-demo-runtime.sh")
|
|
transaction = read(".github/scripts/demo-runtime-transaction.py")
|
|
dispatcher = read(".github/scripts/dispatch-demo-runtime.py")
|
|
|
|
for required in ("workflow_dispatch:", "environment: demo-stable", "contents: read",
|
|
"cancel-in-progress: false", 'gh api "repos/${GITHUB_REPOSITORY}/releases/latest"',
|
|
"Resolve target-compatible recovery profile", "resolve-demo-runtime-profile.sh",
|
|
".github/scripts/recover-demo-runtime.sh", "Verify public health",
|
|
"Verify public frontend parity", "./scripts/run_demo_public_browser_smoke.sh",
|
|
"Capture bounded Pulse failure diagnostics", "demo-runtime-diagnostics.txt",
|
|
"journalctl -u pulse --since '-30 minutes'", "journalctl -u pulse --since '-10 minutes'",
|
|
"tail -400", "Retain bounded recovery evidence", ".forward.elapsed_seconds >= 300"):
|
|
self.assertIn(required, workflow)
|
|
for forbidden in ("workflow_call:", "inputs:", "release-convergence.yml", "install.sh", "scp ",
|
|
"Compensate failed mutated recovery", "sudo systemctl stop pulse"):
|
|
self.assertNotIn(forbidden, workflow)
|
|
self.assertIn('exec python3 .github/scripts/dispatch-demo-runtime.py recover', recovery)
|
|
self.assertIn("if mode == 'update' else b''", dispatcher)
|
|
self.assertIn("/var/lib/pulse-deploy/relay/deploy.lock", transaction)
|
|
self.assertIn('if self.request["mode"] == "update":', transaction)
|
|
self.assertIn('self.host.install(self.attempt, self.request)', transaction)
|
|
self.assertIn('raise Failure("recovery-source-mismatch")', transaction)
|
|
self.assertIn('copy_path(path, snapshot / label)', transaction)
|
|
self.assertIn('copy_path(snapshot / label, PATHS[label])', transaction)
|
|
self.assertIn('estate_hash(PATHS) != self.receipt["snapshot_sha256"]', transaction)
|
|
self.assertIn('WINDOW = 300', transaction)
|
|
self.assertIn('self.watch("recovery", original_version, cursors)', transaction)
|
|
self.assertIn('signal.signal(sig, signal.SIG_IGN)', transaction)
|
|
self.assertIn('outcome = "rolled_back"', transaction)
|
|
self.assertIn('outcome = "rollback_failed"', transaction)
|
|
self.assertIn('self.finish(outcome)', transaction)
|
|
self.assertIn('self.receipt["observed_outcome"] = status', transaction)
|
|
self.assertIn('self.receipt["status"] = "observation_failed"', transaction)
|
|
self.assertIn('self.save(terminal)', transaction)
|
|
for required in ("alerts/events.db", "alerts/alert-history.json.imported",
|
|
"alerts/alert-history.backup.json.imported", "ai_incidents.json", "snapshot_retained"):
|
|
self.assertIn(required, transaction)
|
|
self.assertNotIn('kill -QUIT', transaction + recovery + workflow)
|
|
self.assertNotIn('eval ', transaction + recovery)
|
|
self.assertNotIn("'--wait'", dispatcher)
|
|
|
|
|
|
def test_blocked_record_tracks_current_target_and_candidate_version(self) -> None:
|
|
blocked_record_surface = {
|
|
"VERSION",
|
|
"docs/release-control/control_plane.json",
|
|
"docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md",
|
|
"docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md",
|
|
"scripts/release_control/record_rc_to_ga_blocked.py",
|
|
}
|
|
if USE_STAGED_GOVERNANCE and not any(path in blocked_record_surface for path in STAGED_FILES):
|
|
self.skipTest("staged slice does not touch the blocked-record promotion surface")
|
|
blocked = read("docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md")
|
|
current_version = read("VERSION").strip()
|
|
active_target_id = read_json("docs/release-control/control_plane.json")["active_target_id"]
|
|
if stable_packet_paths_for_version(current_version) is not None:
|
|
self.assertIn("VERSION=6.0.0", blocked)
|
|
if current_version != "6.0.0":
|
|
self.assertNotIn(f"VERSION={current_version}", blocked)
|
|
else:
|
|
self.assertTrue(
|
|
rc_packet_paths_for_version(current_version) is not None
|
|
or support_prerelease_packet_paths_for_version(current_version) is not None,
|
|
f"VERSION={current_version} does not match a governed v6 prerelease packet pattern",
|
|
)
|
|
self.assertIn("VERSION=6.0.0", blocked)
|
|
self.assertNotIn(f"VERSION={current_version}", blocked)
|
|
self.assertIn("artifact-owned candidate stable tag", blocked)
|
|
self.assertIn("artifact-owned promotion channel", blocked)
|
|
self.assertIn("artifact-owned promoted prerelease tag", blocked)
|
|
self.assertIn("artifact-owned rollback target", blocked)
|
|
self.assertIn("Materialize the final rehearsal record from that artifact without", blocked)
|
|
self.assertIn("hand-repairing any missing candidate tag, promoted prerelease tag, rollback", blocked)
|
|
if active_target_id == "v6-ga-promotion":
|
|
self.assertIn(
|
|
f"The active control-plane target is `{active_target_id}`, so stable or GA",
|
|
blocked,
|
|
)
|
|
elif active_target_id in {"v6-product-lane-expansion", "v6-release-reliability"}:
|
|
self.assertIn(
|
|
"The active control-plane target is `v6-ga-promotion`, so stable or GA",
|
|
blocked,
|
|
)
|
|
else:
|
|
self.assertIn(f"The active control-plane target is still `{active_target_id}`, not", blocked)
|
|
matrix = read("docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md")
|
|
self.assertIn(promotion_metadata_envelope(), normalize_ws(matrix))
|
|
expected = blocked_record.build_blocked_record(record_date="2026-04-04")
|
|
if current_version != "6.0.0" or active_target_id != "v6-ga-promotion":
|
|
return
|
|
if blocked != expected:
|
|
record_path = REPO_ROOT / "docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md"
|
|
if os.environ.get("BLESS_GOVERNANCE_FIXTURES") == "1":
|
|
record_path.write_text(expected, encoding="utf-8")
|
|
self.skipTest(
|
|
"Regenerated rc-to-ga-promotion-readiness-blocked-2026-04-04.md "
|
|
"under BLESS_GOVERNANCE_FIXTURES=1; stage the file and rerun without the env var."
|
|
)
|
|
self.fail(
|
|
"Blocked-record fixture drifted from build_blocked_record() output. "
|
|
"This usually means VERSION bumped or a new RC tag landed since the "
|
|
"fixture was last regenerated. To fix, run either:\n"
|
|
" python3 scripts/release_control/record_rc_to_ga_blocked.py "
|
|
"--output docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md "
|
|
"--record-date 2026-04-04\n"
|
|
" (or)\n"
|
|
" BLESS_GOVERNANCE_FIXTURES=1 python3 -m unittest release_promotion_policy_test"
|
|
)
|
|
|
|
def test_routine_stable_patch_entrypoint_is_noninteractive_and_integrated(self) -> None:
|
|
helper = read("scripts/trigger-stable-patch.sh")
|
|
policy = read("docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md")
|
|
contract = read("docs/release-control/v6/internal/subsystems/deployment-installability.md")
|
|
|
|
self.assertNotIn("read -r", helper)
|
|
self.assertNotIn("read -p", helper)
|
|
self.assertIn("--dry-run", helper)
|
|
self.assertIn("--derive-rollback-latest-stable", helper)
|
|
self.assertIn("docs/releases/RELEASE_NOTES_v${VERSION}.md", helper)
|
|
self.assertIn("Use --dry-run only", helper)
|
|
self.assertNotIn("timedelta(hours=24)", helper)
|
|
self.assertNotIn(".createdAt >= $cutoff", helper)
|
|
self.assertIn("gh workflow run create-release.yml", helper)
|
|
self.assertIn("gh workflow run \"$WORKFLOW\"", helper)
|
|
self.assertIn("--unsigned-windows-exception-reason", helper)
|
|
self.assertIn("--unsigned-windows-exception", helper)
|
|
self.assertIn("unsigned_windows_exception", helper)
|
|
self.assertIn("unsigned_windows_reason", helper)
|
|
self.assertIn('--arg hotfix_exception "$HOTFIX_EXCEPTION"', helper)
|
|
self.assertIn(
|
|
'--arg unsigned_windows_exception "$UNSIGNED_WINDOWS_EXCEPTION"',
|
|
helper,
|
|
)
|
|
self.assertIn('--arg draft_only "false"', helper)
|
|
self.assertNotIn("--argjson", helper)
|
|
self.assertIn("Single-Build Release Path", policy)
|
|
self.assertIn("## Stable Patch Path", policy)
|
|
self.assertIn("single publish workflow performs the exact-SHA preflight", normalize_ws(policy))
|
|
self.assertIn("An asynchronous dispatch or manual SSH deployment is not release completion.", normalize_ws(contract))
|
|
|
|
|
|
class MaterialApprovalEvidenceTest(unittest.TestCase):
|
|
def test_source_of_truth_requires_quantitative_approval_provenance(self) -> None:
|
|
content = normalize_ws(
|
|
read("docs/release-control/v6/internal/SOURCE_OF_TRUTH.md")
|
|
)
|
|
self.assertIn(
|
|
"material owner-approval record that relies on quantitative rationale",
|
|
content,
|
|
)
|
|
self.assertIn(
|
|
"durable source, exact query or canonical report command, sanitized snapshot, and measurement time",
|
|
content,
|
|
)
|
|
self.assertIn("separate durable owner-confirmation artifact", content)
|
|
|
|
def test_quantitative_owner_approval_requires_reproducible_provenance(self) -> None:
|
|
content = """# Material decision
|
|
|
|
The release owner explicitly approved this change because weekly active installs
|
|
grew from 100 to 1,000 and conversion reached 7.9%.
|
|
"""
|
|
self.assertEqual(
|
|
material_approval_evidence_errors(content),
|
|
("missing '## Quantitative Evidence' section",),
|
|
)
|
|
|
|
def test_quantitative_evidence_rejects_placeholder_provenance(self) -> None:
|
|
content = """# Material decision
|
|
|
|
Approved by the project owner after conversion reached 7.9%.
|
|
|
|
## Quantitative Evidence
|
|
|
|
- Source: unavailable
|
|
- Query: `scripts/report.py --as-of 2026-08-08`
|
|
- Snapshot: `records/report-2026-08-08.json`
|
|
- Measured at: `2026-08-08T01:47:00Z`
|
|
"""
|
|
self.assertEqual(
|
|
material_approval_evidence_errors(content),
|
|
("'- Source:' must identify durable reproducible evidence",),
|
|
)
|
|
|
|
def test_quantitative_evidence_rejects_unreferenced_field_values(self) -> None:
|
|
content = """# Material decision
|
|
|
|
The product owner explicitly approved this change after a cohort reached 7.9%.
|
|
|
|
## Quantitative Evidence
|
|
|
|
- Source: production telemetry
|
|
- Query: weekly conversion report
|
|
- Snapshot: sanitized aggregate results
|
|
- Measured at: `2026-08-08T01:47:00Z`
|
|
"""
|
|
self.assertEqual(
|
|
material_approval_evidence_errors(content),
|
|
(
|
|
"'- Source:' must contain an inline durable reference",
|
|
"'- Query:' must contain an inline durable reference",
|
|
"'- Snapshot:' must contain an inline durable reference",
|
|
),
|
|
)
|
|
|
|
def test_quantitative_evidence_rejects_missing_local_artifact(self) -> None:
|
|
content = """# Material decision
|
|
|
|
Approved by the project owner after conversion reached 7.9%.
|
|
|
|
## Quantitative Evidence
|
|
|
|
- Source: `missing-evidence.md`
|
|
- Query: `missing-evidence.md#query`
|
|
- Snapshot: `missing-evidence.md#snapshot`
|
|
- Measured at: `2026-08-08T01:47:00Z`
|
|
"""
|
|
record_rel = "docs/release-control/v6/internal/records/example.md"
|
|
self.assertEqual(
|
|
material_approval_evidence_errors(content, record_rel=record_rel),
|
|
(
|
|
"'- Source:' references missing artifact 'docs/release-control/v6/internal/records/missing-evidence.md'",
|
|
"'- Query:' references missing artifact 'docs/release-control/v6/internal/records/missing-evidence.md'",
|
|
"'- Snapshot:' references missing artifact 'docs/release-control/v6/internal/records/missing-evidence.md'",
|
|
),
|
|
)
|
|
|
|
def test_quantitative_evidence_accepts_complete_provenance(self) -> None:
|
|
content = """# Material decision
|
|
|
|
The product owner explicitly approved this change after a cohort reached 7.9%.
|
|
|
|
## Quantitative Evidence
|
|
|
|
- Source: `pulse-pro:license-server/telemetry.sqlite`
|
|
- Query: `scripts/report.py --as-of 2026-08-08`
|
|
- Snapshot: `records/report-2026-08-08.json`
|
|
- Measured at: `2026-08-08T01:47:00Z`
|
|
"""
|
|
self.assertEqual(material_approval_evidence_errors(content), ())
|
|
|
|
def test_tracked_material_approval_records_have_reproducible_provenance(self) -> None:
|
|
failures: list[str] = []
|
|
for rel in tracked_release_control_records():
|
|
errors = material_approval_evidence_errors(read(rel), record_rel=rel)
|
|
failures.extend(f"{rel}: {error}" for error in errors)
|
|
self.assertEqual(failures, [], "\n".join(failures))
|
|
|
|
|
|
class CandidatePublicationBoundaryTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.jobs = yaml.load(read(".github/workflows/create-release.yml"), Loader=UniqueKeyLoader)["jobs"]
|
|
|
|
def test_private_pro_pair_is_verified_before_any_draft_exists(self) -> None:
|
|
prepare_steps = self.jobs["prepare"]["steps"]
|
|
step = next(
|
|
(candidate for candidate in prepare_steps
|
|
if candidate.get("name") == "Verify the private Pro source pair is declared"),
|
|
None,
|
|
)
|
|
self.assertIsNotNone(step, "prepare must verify the private Pro source pair before dispatch")
|
|
condition = step["if"]
|
|
self.assertIn("startsWith(github.event.inputs.version, '6.')", condition)
|
|
self.assertIn("historical_asset_backfill_only != 'true'", condition)
|
|
self.assertIn("draft_only != 'true'", condition)
|
|
self.assertEqual(step["env"]["PRIVATE_PAIR_REPOSITORY"], "rcourtman/pulse-enterprise")
|
|
script = step["run"]
|
|
self.assertIn("docs/release-source-pairs/${EXPECTED_SOURCE_SHA}.json", script)
|
|
self.assertIn("pulse_sha", script)
|
|
self.assertIn("exit 1", script)
|
|
# The draft is created only after the prepare gate can refuse the run.
|
|
self.assertIn("prepare", self.jobs["create_release"]["needs"])
|
|
|
|
def condition(self, job: str, results: dict[str, str], *, draft: bool = False, cancelled: bool = False) -> bool:
|
|
# Execute the workflow's Boolean condition with explicit job outcomes.
|
|
# This intentionally fails if the workflow adds an unsupported expression.
|
|
expression = self.jobs[job]["if"].removeprefix("${{").removesuffix("}}").strip()
|
|
expression = re.sub(r"needs\.([a-z_]+)\.result", lambda m: repr(results[m[1]]), expression)
|
|
expression = expression.replace("needs.prepare.outputs.version", repr("6.4.4-beta.9"))
|
|
expression = expression.replace("needs.prepare.outputs.historical_asset_backfill_only", repr("false"))
|
|
expression = expression.replace("github.event.inputs.draft_only", repr(str(draft).lower()))
|
|
expression = expression.replace("cancelled()", repr(cancelled))
|
|
expression = expression.replace("always()", "True").replace("&&", " and ").replace("||", " or ")
|
|
expression = re.sub(r"!(?!=)", "not ", expression)
|
|
return bool(eval(expression, {"__builtins__": {}, "startsWith": lambda value, prefix: value.startswith(prefix)}))
|
|
|
|
CANDIDATE_CHECKS = frozenset({
|
|
"prepare", "publication_trust_preflight", "build_release_candidate",
|
|
"qualify_release_containers", "frontend_bundle", "frontend_checks",
|
|
"windows_install_command_smoke", "backend_tests", "integration_tests",
|
|
"release_smoke", "create_release", "validate_release_assets",
|
|
"install_sh_smoke", "stage_private_pro_runtime",
|
|
})
|
|
PUBLIC_WRITERS = ("publish_release_tag", "publish_docker", "publish_helm_chart", "activate_release")
|
|
|
|
def test_qualified_beta_tag_survives_intentionally_skipped_ancestor(self) -> None:
|
|
outcomes = dict.fromkeys(self.jobs, "success")
|
|
outcomes["integration_tests"] = "skipped"
|
|
# Actions applies implicit success() when no status function is present.
|
|
# A skipped ancestor would then prevent every writer despite the
|
|
# explicit candidate predicate. Model that status gate as well.
|
|
for name in self.PUBLIC_WRITERS:
|
|
writer = self.jobs[name]
|
|
has_status = bool(re.search(r"\b(always|success|failure|cancelled)\(", writer["if"]))
|
|
with self.subTest(writer=name):
|
|
self.assertTrue(has_status and self.condition(name, outcomes))
|
|
for dependency in writer["needs"]:
|
|
for state in ("failure", "cancelled", "skipped"):
|
|
if dependency == "integration_tests" and state == "skipped":
|
|
continue # Already skipped above, by policy.
|
|
with self.subTest(writer=name, dependency=dependency, state=state):
|
|
self.assertFalse(self.condition(name, outcomes | {dependency: state}))
|
|
|
|
def test_workflow_cancellation_blocks_completed_prerequisite_writers(self) -> None:
|
|
good = dict.fromkeys(self.jobs, "success")
|
|
for writer in ("publish_release_tag", "publish_docker", "publish_helm_chart",
|
|
"dispatch_release_convergence", "activate_release"):
|
|
with self.subTest(writer=writer):
|
|
self.assertTrue(self.condition(writer, good))
|
|
# Cancellation is workflow state, not a changed needs.result:
|
|
# all prerequisites may already have completed successfully.
|
|
self.assertFalse(self.condition(writer, good, cancelled=True))
|
|
# Keep the final evidence/verdict join available after cancellation.
|
|
self.assertTrue(self.condition("release_commit_verdict", good, cancelled=True))
|
|
|
|
def test_failed_candidate_cannot_reach_any_public_version_writer(self) -> None:
|
|
required = set(self.CANDIDATE_CHECKS)
|
|
# The echo-only candidate and readiness joins were folded into the
|
|
# writers. Each writer depends on, and judges, every candidate check
|
|
# itself instead of trusting a join job's result.
|
|
self.assertNotIn("candidate_qualification", self.jobs)
|
|
self.assertNotIn("release_readiness", self.jobs)
|
|
tag_predicate = self.jobs["publish_release_tag"]["if"].removesuffix("}}").strip()
|
|
self.assertEqual(set(self.jobs["publish_release_tag"]["needs"]), required)
|
|
good = dict.fromkeys(self.jobs, "success")
|
|
for writer in self.PUBLIC_WRITERS:
|
|
with self.subTest(writer=writer):
|
|
self.assertTrue(required <= set(self.jobs[writer]["needs"]))
|
|
# Every writer repeats the tag's exact candidate predicate.
|
|
self.assertTrue(self.jobs[writer]["if"].startswith(tag_predicate))
|
|
self.assertTrue(self.condition(writer, good))
|
|
self.assertFalse(self.condition(writer, good, draft=True))
|
|
for failed in required:
|
|
for state in ("failure", "cancelled", "skipped"):
|
|
if failed == "integration_tests" and state == "skipped":
|
|
continue # Existing alpha/beta policy omits integration tests.
|
|
with self.subTest(writer=writer, failed=failed, state=state):
|
|
self.assertFalse(self.condition(writer, good | {failed: state}))
|
|
# Old shape: release_readiness also required the tag and both registry
|
|
# publications before activation. The new activation predicate must
|
|
# match the old readiness-gated one for every single-job outcome.
|
|
for writer in ("publish_docker", "publish_helm_chart", "activate_release"):
|
|
for state in ("failure", "cancelled", "skipped"):
|
|
with self.subTest(writer=writer, tag=state):
|
|
self.assertFalse(self.condition(writer, good | {"publish_release_tag": state}))
|
|
for dependency in ("publish_docker", "publish_helm_chart", "dispatch_release_convergence"):
|
|
for state in ("failure", "cancelled", "skipped"):
|
|
with self.subTest(activation_dependency=dependency, state=state):
|
|
self.assertFalse(self.condition("activate_release", good | {dependency: state}))
|
|
# Detect accidental dependency cycles, including moving publication into
|
|
# the candidate join that publication itself must wait for.
|
|
def visit(name: str, stack: tuple[str, ...] = ()) -> None:
|
|
self.assertNotIn(name, stack)
|
|
deps = self.jobs[name].get("needs", [])
|
|
for dependency in ([deps] if isinstance(deps, str) else deps):
|
|
visit(dependency, (*stack, name))
|
|
for name in self.jobs:
|
|
visit(name)
|
|
|
|
def test_draft_never_pushes_a_tag_and_publication_never_retargets(self) -> None:
|
|
draft = self.jobs["create_release"]
|
|
self.assertFalse(draft["steps"][0]["with"]["persist-credentials"])
|
|
self.assertTrue(self.jobs["publish_release_tag"]["steps"][0]["with"]["persist-credentials"])
|
|
scripts = "\n".join(step.get("run", "") for step in draft["steps"])
|
|
self.assertNotRegex(scripts, r"git (?:push|tag)\b")
|
|
self.assertIn('--arg target_commitish "$HEAD_SHA"', scripts)
|
|
checks = [
|
|
next(step["run"] for step in draft["steps"] if step["name"] == "Check existing public tag without changing it"),
|
|
self.jobs["publish_release_tag"]["steps"][-1]["run"],
|
|
]
|
|
for check_index, script in enumerate(checks):
|
|
for existing in ("", "a" * 40, "b" * 40):
|
|
with self.subTest(step=check_index, existing=existing), tempfile.TemporaryDirectory() as temp:
|
|
root = Path(temp)
|
|
fake = root / "git"
|
|
fake.write_text("#!/usr/bin/env python3\nimport os,sys,json\na=sys.argv[1:]\n"
|
|
"with open(os.environ['CALL_LOG'],'a') as f: f.write(json.dumps(a)+'\\n')\n"
|
|
"if a[0]=='rev-parse': print('a'*40)\n"
|
|
"elif a[0]=='ls-remote' and os.environ['EXISTING']: print(os.environ['EXISTING']+'\\tref')\n")
|
|
fake.chmod(0o755)
|
|
log = root / "calls"
|
|
env = os.environ | {"PATH": str(root) + os.pathsep + os.environ["PATH"],
|
|
"TAG": "v6.4.4-beta.9", "EXISTING": existing, "CALL_LOG": str(log)}
|
|
result = subprocess.run(["bash", "-c", script], env=env, text=True, capture_output=True)
|
|
self.assertEqual(result.returncode == 0, existing != "b" * 40, result.stderr)
|
|
calls = [json.loads(line) for line in log.read_text().splitlines()]
|
|
writes = [call for call in calls if call[0] in ("tag", "push")]
|
|
if check_index == 1 and not existing:
|
|
self.assertEqual([call[0] for call in writes], ["tag", "push"])
|
|
self.assertNotIn("--force", str(writes))
|
|
else:
|
|
self.assertEqual(writes, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|