Files
pulse/scripts/release_control/release_continuity.py
T
pulse-triage[bot] dc4639460a Bind continuity checks to activation asset bytes
Change-source: pulse-maintainer
2026-09-01 12:47:16 +01:00

570 lines
19 KiB
Python

#!/usr/bin/env python3
"""Validate and explain the public stable-release continuity identity."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any
STABLE_TAG = re.compile(r"^v[0-9]+\.[0-9]+\.[0-9]+$")
SOURCE_SHA = re.compile(r"^[0-9a-f]{40}$")
RUN_ID = re.compile(r"^[0-9]+$")
SHA256 = re.compile(r"^sha256:[0-9a-f]{64}$")
RELEASE_REFERENCE_VIOLATIONS = frozenset(
{
"release_payload_invalid",
"release_id_invalid",
"stable_tag_invalid",
"source_identity_invalid",
}
)
IMMUTABLE_REPLACEMENT_ACTION = (
"Do not edit or repair the advertised release in place; restore the last known-good "
"stable target if needed, then publish a corrected replacement through convergence."
)
@dataclass(frozen=True)
class Violation:
code: str
field: str
expected: str
actual: Any
action: str
def as_dict(self) -> dict[str, Any]:
return {
"code": self.code,
"field": self.field,
"expected": self.expected,
"actual": diagnostic_value(self.actual),
"action": self.action,
}
RELEASE_RULES = {
"release_payload_invalid": (
"GitHub did not return one release object.",
"Inspect the releases/latest API response and API availability before retrying.",
),
"release_id_invalid": (
"The release id is absent or malformed.",
"Do not activate the release; inspect how the release was created.",
),
"stable_tag_invalid": (
"The advertised release is not a stable vX.Y.Z tag.",
"Restore the latest stable pointer to an exact stable release.",
),
"source_identity_invalid": (
"The release is not bound to a full lowercase source commit.",
"Publish from an exact 40-character source commit.",
),
"release_is_draft": (
"The advertised release is still a draft.",
"Keep drafts outside the stable channel until convergence completes.",
),
"release_is_prerelease": (
"The advertised release is marked as a prerelease.",
"Keep prereleases outside the stable latest-release pointer.",
),
"release_mutable": (
"GitHub reports immutable=false for the advertised stable release.",
"Publish a replacement through the immutable-release-gated pipeline; "
"never repair the packet in place.",
),
"publication_time_invalid": (
"The advertised release has no publication timestamp.",
"Do not treat the release as activated until GitHub reports publication.",
),
}
ACTIVATION_RULES = {
"activation_asset_invalid": (
"The release does not list one uploaded, digest-bound activation marker.",
IMMUTABLE_REPLACEMENT_ACTION,
),
"activation_asset_size_mismatch": (
"The downloaded activation marker size differs from its release metadata.",
IMMUTABLE_REPLACEMENT_ACTION,
),
"activation_asset_digest_mismatch": (
"The downloaded activation marker digest differs from its release metadata.",
IMMUTABLE_REPLACEMENT_ACTION,
),
"activation_payload_invalid": (
"The activation marker is not one JSON object.",
IMMUTABLE_REPLACEMENT_ACTION,
),
"activation_schema_invalid": (
"The activation marker schema is unsupported.",
IMMUTABLE_REPLACEMENT_ACTION,
),
"activation_tag_mismatch": (
"The activation marker names a different release tag.",
IMMUTABLE_REPLACEMENT_ACTION,
),
"activation_release_mismatch": (
"The activation marker names a different GitHub release id.",
IMMUTABLE_REPLACEMENT_ACTION,
),
"activation_source_mismatch": (
"The activation marker names a different source commit.",
IMMUTABLE_REPLACEMENT_ACTION,
),
"source_run_invalid": (
"The activation marker has no valid source release run id.",
IMMUTABLE_REPLACEMENT_ACTION,
),
"convergence_run_invalid": (
"The activation marker has no valid convergence run id.",
IMMUTABLE_REPLACEMENT_ACTION,
),
"delivery_prefix_invalid": (
"The activation marker has no customer-delivery prefix.",
IMMUTABLE_REPLACEMENT_ACTION,
),
"server_digest_invalid": (
"The activation marker has no valid server image digest.",
IMMUTABLE_REPLACEMENT_ACTION,
),
"control_plane_digest_invalid": (
"The activation marker has no valid control-plane image digest.",
IMMUTABLE_REPLACEMENT_ACTION,
),
"helm_digest_invalid": (
"The activation marker has no valid Helm chart digest.",
IMMUTABLE_REPLACEMENT_ACTION,
),
}
def diagnostic_value(value: Any) -> Any:
if value is None or isinstance(value, (bool, int, float)):
return value
if isinstance(value, str):
return value if len(value) <= 160 else value[:157] + "..."
return f"<{type(value).__name__}>"
def read_json(path: Path) -> Any:
try:
return parse_json(path.read_bytes(), path)
except OSError as exc:
raise ValueError(f"cannot read JSON from {path}: {exc}") from exc
def parse_json(content: bytes, path: Path) -> Any:
try:
return json.loads(content)
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise ValueError(f"cannot read JSON from {path}: {exc}") from exc
def violation(
code: str, field: str, expected: str, actual: Any, rules: dict[str, tuple[str, str]]
) -> Violation:
_, action = rules[code]
return Violation(code, field, expected, actual, action)
def release_violations(payload: Any) -> list[Violation]:
if not isinstance(payload, dict):
return [
violation(
"release_payload_invalid",
"$",
"object",
payload,
RELEASE_RULES,
)
]
failures: list[Violation] = []
release_id = payload.get("id")
if not isinstance(release_id, int) or isinstance(release_id, bool) or release_id <= 0:
failures.append(
violation("release_id_invalid", "id", "positive integer", release_id, RELEASE_RULES)
)
tag = payload.get("tag_name")
if not isinstance(tag, str) or STABLE_TAG.fullmatch(tag) is None:
failures.append(
violation("stable_tag_invalid", "tag_name", "vX.Y.Z", tag, RELEASE_RULES)
)
source = payload.get("target_commitish")
if not isinstance(source, str) or SOURCE_SHA.fullmatch(source) is None:
failures.append(
violation(
"source_identity_invalid",
"target_commitish",
"40 lowercase hexadecimal characters",
source,
RELEASE_RULES,
)
)
if payload.get("draft") is not False:
failures.append(
violation("release_is_draft", "draft", "false", payload.get("draft"), RELEASE_RULES)
)
if payload.get("prerelease") is not False:
failures.append(
violation(
"release_is_prerelease",
"prerelease",
"false",
payload.get("prerelease"),
RELEASE_RULES,
)
)
if payload.get("immutable") is not True:
failures.append(
violation(
"release_mutable", "immutable", "true", payload.get("immutable"), RELEASE_RULES
)
)
published_at = payload.get("published_at")
if not isinstance(published_at, str) or not published_at:
failures.append(
violation(
"publication_time_invalid",
"published_at",
"non-empty timestamp",
published_at,
RELEASE_RULES,
)
)
return failures
def release_identity(payload: dict[str, Any]) -> dict[str, Any]:
return {
"id": payload["id"],
"tag": payload["tag_name"],
"source_sha": payload["target_commitish"],
"draft": payload["draft"],
"prerelease": payload["prerelease"],
"immutable": payload["immutable"],
"published_at": payload["published_at"],
}
def release_is_referenceable(payload: Any, failures: list[Violation]) -> bool:
"""Return whether the release can be looked up without trusting it.
A mutable, draft, prerelease, or incompletely published release must still
fail continuity admission. Its validated tag, numeric release ID, and exact
source SHA are nevertheless safe lookup data for collecting independent
activation diagnostics. Keeping this distinction explicit prevents a
first failure from hiding additional damage while preserving fail-closed
delivery gates.
"""
return isinstance(payload, dict) and not any(
item.code in RELEASE_REFERENCE_VIOLATIONS for item in failures
)
def activation_violations(
payload: Any, expected_release: dict[str, Any], activation_bytes: bytes
) -> list[Violation]:
failures: list[Violation] = []
assets = expected_release.get("assets")
activation_assets = (
[
asset
for asset in assets
if isinstance(asset, dict) and asset.get("name") == "release-activation.json"
]
if isinstance(assets, list)
else []
)
if len(activation_assets) != 1:
failures.append(
violation(
"activation_asset_invalid",
"assets.release-activation.json",
"one uploaded non-empty asset with a SHA-256 digest",
assets,
ACTIVATION_RULES,
)
)
else:
asset = activation_assets[0]
asset_size = asset.get("size")
asset_digest = asset.get("digest")
if (
asset.get("state") != "uploaded"
or not isinstance(asset_size, int)
or isinstance(asset_size, bool)
or asset_size <= 0
or not isinstance(asset_digest, str)
or SHA256.fullmatch(asset_digest) is None
):
failures.append(
violation(
"activation_asset_invalid",
"assets.release-activation.json",
"one uploaded non-empty asset with a SHA-256 digest",
asset,
ACTIVATION_RULES,
)
)
else:
actual_size = len(activation_bytes)
actual_digest = "sha256:" + hashlib.sha256(activation_bytes).hexdigest()
if actual_size != asset_size:
failures.append(
violation(
"activation_asset_size_mismatch",
"assets.release-activation.json.size",
str(asset_size),
actual_size,
ACTIVATION_RULES,
)
)
if actual_digest != asset_digest:
failures.append(
violation(
"activation_asset_digest_mismatch",
"assets.release-activation.json.digest",
asset_digest,
actual_digest,
ACTIVATION_RULES,
)
)
if not isinstance(payload, dict):
failures.append(
violation(
"activation_payload_invalid",
"$",
"object",
payload,
ACTIVATION_RULES,
)
)
return failures
checks = (
("activation_schema_invalid", "schema_version", 1, "integer 1"),
(
"activation_tag_mismatch",
"tag",
expected_release["tag_name"],
expected_release["tag_name"],
),
(
"activation_release_mismatch",
"release_id",
str(expected_release["id"]),
str(expected_release["id"]),
),
(
"activation_source_mismatch",
"target_commitish",
expected_release["target_commitish"],
expected_release["target_commitish"],
),
)
for code, field, expected, expected_description in checks:
actual = payload.get(field)
if actual != expected or (field == "schema_version" and isinstance(actual, bool)):
failures.append(
violation(code, field, str(expected_description), actual, ACTIVATION_RULES)
)
for code, field in (
("source_run_invalid", "source_release_run_id"),
("convergence_run_invalid", "convergence_run_id"),
):
actual = payload.get(field)
if not isinstance(actual, str) or RUN_ID.fullmatch(actual) is None:
failures.append(
violation(code, field, "decimal run id string", actual, ACTIVATION_RULES)
)
prefix = payload.get("r2_prefix")
if not isinstance(prefix, str) or not prefix:
failures.append(
violation(
"delivery_prefix_invalid",
"r2_prefix",
"non-empty string",
prefix,
ACTIVATION_RULES,
)
)
for code, field in (
("server_digest_invalid", "server_image_digest"),
("control_plane_digest_invalid", "control_plane_image_digest"),
("helm_digest_invalid", "helm_chart_digest"),
):
actual = payload.get(field)
if not isinstance(actual, str) or SHA256.fullmatch(actual) is None:
failures.append(
violation(code, field, "sha256:<64 lowercase hex>", actual, ACTIVATION_RULES)
)
return failures
def write_diagnostic(
path: Path,
check: str,
identity: dict[str, Any],
failures: list[Violation],
) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
result = {
"schema_version": 1,
"check": check,
"status": "failure" if failures else "success",
"identity": {key: diagnostic_value(value) for key, value in identity.items()},
"violations": [item.as_dict() for item in failures],
}
path.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def append_outputs(path: Path, outputs: dict[str, str]) -> None:
with path.open("a", encoding="utf-8") as handle:
for key, value in outputs.items():
if "\n" in value or "\r" in value:
raise ValueError(f"output {key} contains a line break")
handle.write(f"{key}={value}\n")
def report_failures(failures: list[Violation], rules: dict[str, tuple[str, str]]) -> None:
for item in failures:
message, action = rules[item.code]
print(
f"::error title=Stable release continuity [{item.code}]::{message} {action}",
file=sys.stderr,
)
def validate_release(args: argparse.Namespace) -> int:
try:
payload = read_json(args.release_json)
except ValueError as exc:
payload = None
failures = [
violation("release_payload_invalid", "$", "object", str(exc), RELEASE_RULES)
]
else:
failures = release_violations(payload)
identity = release_identity(payload) if isinstance(payload, dict) and not failures else {
key: payload.get(source) if isinstance(payload, dict) else None
for key, source in (
("id", "id"),
("tag", "tag_name"),
("source_sha", "target_commitish"),
("draft", "draft"),
("prerelease", "prerelease"),
("immutable", "immutable"),
("published_at", "published_at"),
)
}
write_diagnostic(args.diagnostic, "stable_release_identity", identity, failures)
if release_is_referenceable(payload, failures):
append_outputs(
args.github_output,
{
"referenceable": "true",
"tag": payload["tag_name"],
"release_id": str(payload["id"]),
"source_sha": payload["target_commitish"],
},
)
if failures:
report_failures(failures, RELEASE_RULES)
return 1
return 0
def validate_activation(args: argparse.Namespace) -> int:
try:
release = read_json(args.release_json)
release_failures = release_violations(release)
if not release_is_referenceable(release, release_failures):
raise ValueError("release identity is not safe to reference")
activation_bytes = args.activation_json.read_bytes()
activation = parse_json(activation_bytes, args.activation_json)
except (OSError, ValueError) as exc:
activation = None
failures = [
violation("activation_payload_invalid", "$", "object", str(exc), ACTIVATION_RULES)
]
identity: dict[str, Any] = {}
else:
failures = activation_violations(activation, release, activation_bytes)
identity = (
{
key: activation.get(key)
for key in (
"schema_version",
"tag",
"release_id",
"target_commitish",
"source_release_run_id",
"convergence_run_id",
"r2_prefix",
"server_image_digest",
"control_plane_image_digest",
"helm_chart_digest",
)
}
if isinstance(activation, dict)
else {}
)
write_diagnostic(args.diagnostic, "release_activation_binding", identity, failures)
if failures:
report_failures(failures, ACTIVATION_RULES)
return 1
append_outputs(
args.github_output,
{
"activation_sha256": hashlib.sha256(activation_bytes).hexdigest(),
"server_image_digest": activation["server_image_digest"],
"control_plane_image_digest": activation["control_plane_image_digest"],
"helm_chart_digest": activation["helm_chart_digest"],
},
)
return 0
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
release = commands.add_parser("release")
release.add_argument("--release-json", type=Path, required=True)
release.add_argument("--diagnostic", type=Path, required=True)
release.add_argument("--github-output", type=Path, required=True)
activation = commands.add_parser("activation")
activation.add_argument("--release-json", type=Path, required=True)
activation.add_argument("--activation-json", type=Path, required=True)
activation.add_argument("--diagnostic", type=Path, required=True)
activation.add_argument("--github-output", type=Path, required=True)
return parser.parse_args()
def main() -> int:
args = parse_args()
if args.command == "release":
return validate_release(args)
return validate_activation(args)
if __name__ == "__main__":
raise SystemExit(main())