Some checks failed
CD Pipeline / select-latest-carrier (push) Successful in 48s
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m34s
CD Pipeline / revalidate-deploy-carrier (push) Successful in 26s
CD Pipeline / revalidate-post-deploy-carrier (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
192 lines
5.9 KiB
Python
192 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Select one exact main revision as the inclusive AWOOOI CD carrier."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import time
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
from typing import Sequence
|
|
|
|
|
|
SHA_RE = re.compile(r"^[0-9a-f]{40}$")
|
|
EXCLUDED_MESSAGE_MARKERS = ("[skip ci]", "cancel-stale-cd", "[metadata-only]")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CarrierDecision:
|
|
selected: bool
|
|
status: str
|
|
source_sha: str
|
|
latest_sha: str
|
|
main_tip_sha: str
|
|
|
|
|
|
def _git(args: Sequence[str], *, cwd: Path, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
["git", *args],
|
|
cwd=cwd,
|
|
check=check,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
|
|
def _validate_sha(value: str, *, label: str) -> str:
|
|
normalized = value.strip().lower()
|
|
if not SHA_RE.fullmatch(normalized):
|
|
raise RuntimeError(f"{label}_invalid")
|
|
return normalized
|
|
|
|
|
|
def _latest_remote_sha(*, remote_url: str, branch: str, cwd: Path) -> str:
|
|
result = _git(
|
|
["ls-remote", "--heads", remote_url, f"refs/heads/{branch}"],
|
|
cwd=cwd,
|
|
)
|
|
rows = [row.split() for row in result.stdout.splitlines() if row.strip()]
|
|
if len(rows) != 1 or len(rows[0]) != 2:
|
|
raise RuntimeError("latest_main_ref_unresolved")
|
|
return _validate_sha(rows[0][0], label="latest_sha")
|
|
|
|
|
|
def _fetch_latest_history(*, remote_url: str, branch: str, cwd: Path, depth: int) -> None:
|
|
_git(
|
|
[
|
|
"fetch",
|
|
"--no-tags",
|
|
"--prune",
|
|
f"--depth={depth}",
|
|
remote_url,
|
|
f"+refs/heads/{branch}:refs/remotes/awoooi-carrier/{branch}",
|
|
],
|
|
cwd=cwd,
|
|
)
|
|
|
|
|
|
def _latest_deploy_eligible_sha(*, branch: str, cwd: Path) -> str | None:
|
|
result = _git(
|
|
[
|
|
"log",
|
|
"--format=%H%x1f%B%x1e",
|
|
f"refs/remotes/awoooi-carrier/{branch}",
|
|
],
|
|
cwd=cwd,
|
|
)
|
|
for record in result.stdout.split("\x1e"):
|
|
sha, separator, message = record.strip().partition("\x1f")
|
|
if not separator:
|
|
continue
|
|
normalized_message = message.lower()
|
|
if any(marker in normalized_message for marker in EXCLUDED_MESSAGE_MARKERS):
|
|
continue
|
|
return _validate_sha(sha, label="latest_deploy_eligible_sha")
|
|
return None
|
|
|
|
|
|
def _source_is_ancestor(*, source_sha: str, latest_sha: str, cwd: Path) -> bool:
|
|
result = _git(
|
|
["merge-base", "--is-ancestor", source_sha, latest_sha],
|
|
cwd=cwd,
|
|
check=False,
|
|
)
|
|
if result.returncode not in {0, 1}:
|
|
raise RuntimeError("carrier_lineage_check_failed")
|
|
return result.returncode == 0
|
|
|
|
|
|
def select_latest_inclusive_carrier(
|
|
*,
|
|
source_sha: str,
|
|
remote_url: str,
|
|
branch: str = "main",
|
|
cwd: Path,
|
|
debounce_seconds: float = 0,
|
|
) -> CarrierDecision:
|
|
source_sha = _validate_sha(source_sha, label="source_sha")
|
|
if debounce_seconds < 0 or debounce_seconds > 60:
|
|
raise RuntimeError("debounce_seconds_out_of_range")
|
|
if debounce_seconds:
|
|
time.sleep(debounce_seconds)
|
|
|
|
main_tip_sha = _latest_remote_sha(remote_url=remote_url, branch=branch, cwd=cwd)
|
|
_fetch_latest_history(remote_url=remote_url, branch=branch, cwd=cwd, depth=256)
|
|
latest_sha = _latest_deploy_eligible_sha(branch=branch, cwd=cwd)
|
|
if latest_sha is None:
|
|
_fetch_latest_history(remote_url=remote_url, branch=branch, cwd=cwd, depth=2048)
|
|
latest_sha = _latest_deploy_eligible_sha(branch=branch, cwd=cwd)
|
|
if latest_sha is None:
|
|
raise RuntimeError("latest_deploy_eligible_main_unresolved")
|
|
|
|
if source_sha == latest_sha:
|
|
return CarrierDecision(
|
|
selected=True,
|
|
status="selected_latest_main_carrier",
|
|
source_sha=source_sha,
|
|
latest_sha=latest_sha,
|
|
main_tip_sha=main_tip_sha,
|
|
)
|
|
|
|
if not _source_is_ancestor(source_sha=source_sha, latest_sha=latest_sha, cwd=cwd):
|
|
shallow = _git(
|
|
["rev-parse", "--is-shallow-repository"],
|
|
cwd=cwd,
|
|
).stdout.strip()
|
|
if shallow == "true":
|
|
_fetch_latest_history(remote_url=remote_url, branch=branch, cwd=cwd, depth=2048)
|
|
|
|
if not _source_is_ancestor(source_sha=source_sha, latest_sha=latest_sha, cwd=cwd):
|
|
raise RuntimeError("latest_main_does_not_include_source")
|
|
|
|
return CarrierDecision(
|
|
selected=False,
|
|
status="superseded_by_latest_inclusive_carrier",
|
|
source_sha=source_sha,
|
|
latest_sha=latest_sha,
|
|
main_tip_sha=main_tip_sha,
|
|
)
|
|
|
|
|
|
def _write_github_output(path: Path, decision: CarrierDecision) -> None:
|
|
fields = {
|
|
"selected": str(decision.selected).lower(),
|
|
"status": decision.status,
|
|
"source_sha": decision.source_sha,
|
|
"latest_sha": decision.latest_sha,
|
|
"main_tip_sha": decision.main_tip_sha,
|
|
}
|
|
with path.open("a", encoding="utf-8") as stream:
|
|
for key, value in fields.items():
|
|
stream.write(f"{key}={value}\n")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--source-sha", required=True)
|
|
parser.add_argument("--remote-url", required=True)
|
|
parser.add_argument("--branch", default="main")
|
|
parser.add_argument("--repo", type=Path, default=Path.cwd())
|
|
parser.add_argument("--debounce-seconds", type=float, default=0)
|
|
parser.add_argument("--github-output", type=Path)
|
|
args = parser.parse_args()
|
|
|
|
decision = select_latest_inclusive_carrier(
|
|
source_sha=args.source_sha,
|
|
remote_url=args.remote_url,
|
|
branch=args.branch,
|
|
cwd=args.repo.resolve(),
|
|
debounce_seconds=args.debounce_seconds,
|
|
)
|
|
if args.github_output:
|
|
_write_github_output(args.github_output, decision)
|
|
print(json.dumps(asdict(decision), sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|