Pulse/scripts/release_control/validate_artifact_release_line.py
rcourtman 4fcb90673b Allow stable patch artifact publishing
Reuse shared release-line validation for Docker, floating-tag, and Helm artifact workflows so stable patch tags can publish from the previous stable tag without a fabricated same-version RC.
2026-07-04 20:49:47 +01:00

176 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""Validate public artifact publication against the governed release line."""
from __future__ import annotations
import argparse
import subprocess
from pathlib import Path
from typing import Callable
from control_plane import release_branch_for_version
from repo_file_io import REPO_ROOT, git_env
from resolve_release_promotion import SEMVER_STABLE_RE, is_prerelease_version, normalize_tag
def run_git(args: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", *args],
cwd=REPO_ROOT,
env=git_env(),
check=check,
capture_output=True,
text=True,
)
def ensure_release_refs(required_branch: str) -> None:
shallow = run_git(["rev-parse", "--is-shallow-repository"]).stdout.strip()
if shallow == "true":
run_git(["fetch", "--prune", "--unshallow", "origin"])
run_git(["fetch", "--prune", "origin", required_branch])
def tag_exists(tag: str) -> bool:
result = run_git(["rev-parse", "-q", "--verify", f"refs/tags/{tag}"], check=False)
return result.returncode == 0
def tag_commit(tag: str) -> str:
return run_git(["rev-list", "-n1", f"refs/tags/{tag}"]).stdout.strip()
def ref_commit(ref: str) -> str:
return run_git(["rev-list", "-n1", ref]).stdout.strip()
def ref_is_ancestor(ancestor: str, descendant: str) -> bool:
result = run_git(["merge-base", "--is-ancestor", ancestor, descendant], check=False)
return result.returncode == 0
def matching_prerelease_tags(version: str) -> tuple[str, ...]:
output = run_git(["tag", "-l", f"v{version}-rc.*", "--sort=-version:refname"]).stdout
return tuple(line.strip() for line in output.splitlines() if line.strip())
def previous_stable_tag(version: str) -> str:
match = SEMVER_STABLE_RE.match(version)
if not match:
return ""
major, minor, patch = (int(part) for part in match.groups())
if patch <= 0:
return ""
return f"v{major}.{minor}.{patch - 1}"
def validate_artifact_release_line(
*,
tag: str,
purpose: str,
branch_for_version_fn: Callable[[str], str] = release_branch_for_version,
fetch_refs_fn: Callable[[str], None] = ensure_release_refs,
tag_exists_fn: Callable[[str], bool] = tag_exists,
tag_commit_fn: Callable[[str], str] = tag_commit,
ref_commit_fn: Callable[[str], str] = ref_commit,
ref_is_ancestor_fn: Callable[[str, str], bool] = ref_is_ancestor,
prerelease_tags_fn: Callable[[str], tuple[str, ...]] = matching_prerelease_tags,
) -> dict[str, str]:
normalized_tag = normalize_tag(tag)
if not normalized_tag:
raise ValueError("release tag is required")
version = normalized_tag.removeprefix("v")
required_branch = branch_for_version_fn(version)
fetch_refs_fn(required_branch)
if not tag_exists_fn(normalized_tag):
raise ValueError(f"Tag {normalized_tag} does not exist in repository tags.")
tag_sha = tag_commit_fn(normalized_tag)
branch_ref = f"origin/{required_branch}"
branch_sha = ref_commit_fn(branch_ref)
if not ref_is_ancestor_fn(tag_sha, branch_sha):
raise ValueError(
f"Tag {normalized_tag} is not reachable from {branch_ref}. Refusing {purpose}."
)
lineage = "prerelease"
lineage_tag = ""
if not is_prerelease_version(version):
lineage = ""
for rc_tag in prerelease_tags_fn(version):
rc_sha = tag_commit_fn(rc_tag)
if ref_is_ancestor_fn(rc_sha, tag_sha):
lineage = "promoted_prerelease"
lineage_tag = rc_tag
break
if not lineage:
previous_tag = previous_stable_tag(version)
if previous_tag and tag_exists_fn(previous_tag):
previous_sha = tag_commit_fn(previous_tag)
if ref_is_ancestor_fn(previous_sha, tag_sha):
lineage = "stable_patch"
lineage_tag = previous_tag
if not lineage:
previous_tag = previous_stable_tag(version)
if previous_tag:
raise ValueError(
f"Stable patch tag {normalized_tag} must descend from a matching prerelease tag "
f"or previous stable tag {previous_tag}."
)
raise ValueError(
f"Stable tag {normalized_tag} does not descend from any matching prerelease tag "
f"for base version {version}."
)
return {
"tag": normalized_tag,
"version": version,
"required_branch": required_branch,
"lineage": lineage,
"lineage_tag": lineage_tag,
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--tag", required=True)
parser.add_argument("--purpose", default="artifact publication")
parser.add_argument("--github-output", default="")
return parser.parse_args()
def write_github_output(path: str, values: dict[str, str]) -> None:
if not path:
return
with Path(path).open("a", encoding="utf-8") as output:
for key, value in values.items():
output.write(f"{key}={value}\n")
def main() -> int:
args = parse_args()
result = validate_artifact_release_line(tag=args.tag, purpose=args.purpose)
if result["lineage"] == "promoted_prerelease":
print(f"[OK] {result['tag']} descends from prerelease {result['lineage_tag']}")
elif result["lineage"] == "stable_patch":
print(f"[OK] {result['tag']} descends from previous stable {result['lineage_tag']}")
print(f"[OK] {result['tag']} validated against release line {result['required_branch']}")
write_github_output(
args.github_output,
{
"required_branch": result["required_branch"],
"lineage": result["lineage"],
"lineage_tag": result["lineage_tag"],
},
)
return 0
if __name__ == "__main__":
raise SystemExit(main())