mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 08:23:23 +00:00
176 lines
5.6 KiB
Python
176 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate the explicit mobile-release decision for governed release dispatch."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
VALID_DECISIONS = (
|
|
"no-mobile-impact",
|
|
"existing-mobile-build-compatible",
|
|
"mobile-candidate-uploaded",
|
|
"mobile-candidate-required",
|
|
)
|
|
|
|
EVIDENCE_REQUIRED_DECISIONS = {
|
|
"existing-mobile-build-compatible",
|
|
"mobile-candidate-uploaded",
|
|
}
|
|
|
|
BLOCKING_DECISION = "mobile-candidate-required"
|
|
|
|
|
|
def normalize_decision(value: str) -> str:
|
|
return value.strip().lower().replace("_", "-")
|
|
|
|
|
|
def validate_mobile_release_decision(
|
|
decision: str,
|
|
evidence: str,
|
|
) -> list[str]:
|
|
normalized = normalize_decision(decision)
|
|
trimmed_evidence = evidence.strip()
|
|
errors: list[str] = []
|
|
|
|
if not normalized:
|
|
return [
|
|
"mobile_release_decision is required. Choose one of: "
|
|
+ ", ".join(VALID_DECISIONS)
|
|
]
|
|
|
|
if normalized not in VALID_DECISIONS:
|
|
return [
|
|
f"unknown mobile_release_decision {decision!r}. Choose one of: "
|
|
+ ", ".join(VALID_DECISIONS)
|
|
]
|
|
|
|
if normalized == BLOCKING_DECISION:
|
|
errors.append(
|
|
"mobile-candidate-required is a blocking release state. "
|
|
"Build and submit the required mobile candidate first, then rerun "
|
|
"with mobile-candidate-uploaded and evidence."
|
|
)
|
|
|
|
if normalized in EVIDENCE_REQUIRED_DECISIONS and not trimmed_evidence:
|
|
errors.append(
|
|
f"mobile_release_evidence is required for {normalized}. "
|
|
"Record the compatible build proof or the uploaded candidate build."
|
|
)
|
|
|
|
return errors
|
|
|
|
|
|
def _run_git(repo: Path, args: list[str]) -> str:
|
|
try:
|
|
completed = subprocess.run(
|
|
["git", "-C", str(repo), *args],
|
|
check=True,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
except (OSError, subprocess.CalledProcessError):
|
|
return ""
|
|
return completed.stdout.strip()
|
|
|
|
|
|
def _read_json(path: Path) -> dict[str, object]:
|
|
try:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError):
|
|
return {}
|
|
|
|
|
|
def summarize_mobile_repo(repo: Path) -> str:
|
|
lines: list[str] = [f"Pulse Mobile release context: {repo}"]
|
|
|
|
if not (repo / ".git").exists():
|
|
lines.append(" pulse-mobile repo not found; make an explicit mobile release decision manually.")
|
|
return "\n".join(lines)
|
|
|
|
app = _read_json(repo / "app.json")
|
|
expo = app.get("expo") if isinstance(app.get("expo"), dict) else {}
|
|
ios = expo.get("ios") if isinstance(expo.get("ios"), dict) else {}
|
|
android = expo.get("android") if isinstance(expo.get("android"), dict) else {}
|
|
readiness = _read_json(repo / "store" / "release-readiness.json")
|
|
current = readiness.get("currentCandidate") if isinstance(readiness.get("currentCandidate"), dict) else {}
|
|
|
|
lines.append(
|
|
" app.json: "
|
|
f"version={expo.get('version', '-')}, "
|
|
f"iosBuildNumber={ios.get('buildNumber', '-')}, "
|
|
f"androidVersionCode={android.get('versionCode', '-')}"
|
|
)
|
|
lines.append(
|
|
" release-readiness currentCandidate: "
|
|
f"marketingVersion={current.get('marketingVersion', '-')}, "
|
|
f"iosBuildNumber={current.get('iosBuildNumber', '-')}, "
|
|
f"androidVersionCode={current.get('androidVersionCode', '-')}, "
|
|
f"recordedOn={current.get('recordedOn', '-')}"
|
|
)
|
|
|
|
recorded_on = str(current.get("recordedOn") or "").strip()
|
|
if recorded_on:
|
|
recent = _run_git(
|
|
repo,
|
|
["log", f"--since={recorded_on}T00:00:00", "--oneline", "--max-count=8"],
|
|
)
|
|
if recent:
|
|
lines.append(" commits since recorded candidate:")
|
|
lines.extend(f" {line}" for line in recent.splitlines())
|
|
else:
|
|
lines.append(" commits since recorded candidate: none")
|
|
else:
|
|
recent = _run_git(repo, ["log", "--oneline", "--max-count=5"])
|
|
if recent:
|
|
lines.append(" recent commits:")
|
|
lines.extend(f" {line}" for line in recent.splitlines())
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Validate the mobile impact decision required by release dispatch."
|
|
)
|
|
parser.add_argument("--decision", default="")
|
|
parser.add_argument("--evidence", default="")
|
|
parser.add_argument("--version", default="")
|
|
parser.add_argument("--mobile-repo", default="")
|
|
parser.add_argument("--summary-only", action="store_true")
|
|
parser.add_argument("--github-annotations", action="store_true")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
|
|
if args.mobile_repo:
|
|
print(summarize_mobile_repo(Path(args.mobile_repo)))
|
|
if args.summary_only:
|
|
return 0
|
|
|
|
errors = validate_mobile_release_decision(args.decision, args.evidence)
|
|
if errors:
|
|
for error in errors:
|
|
if args.github_annotations:
|
|
print(f"::error::{error}", file=sys.stderr)
|
|
else:
|
|
print(f"ERROR: {error}", file=sys.stderr)
|
|
return 1
|
|
|
|
normalized = normalize_decision(args.decision)
|
|
version_context = f" for {args.version}" if args.version else ""
|
|
print(f"[OK] Mobile release decision{version_context}: {normalized}")
|
|
if args.evidence.strip():
|
|
print(f"[OK] Mobile release evidence: {args.evidence.strip()}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|