"""Resolver for the production release SHA. Gitea responses are local fixtures.""" from __future__ import annotations import json import os import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SCRIPT = ROOT / "deploy" / "resolve-release-revision.sh" GATED = ROOT / "deploy" / "gated-paths.txt" PYTHON = ROOT / ".venv" / "Scripts" / "python.exe" GIT_BASH = Path(r"D:\Program Files\Git\bin\bash.exe") BASH = str(GIT_BASH) if GIT_BASH.exists() else "bash" def run_git(repo: Path, *args: str) -> str: completed = subprocess.run( ["git", "-c", "user.email=dev@example.com", "-c", "user.name=Release Test", *args], cwd=repo, check=True, capture_output=True, text=True, encoding="utf-8", ) return completed.stdout.strip() def make_repo(tmp_path: Path) -> dict[str, str]: repo = tmp_path / "repo" repo.mkdir() run_git(repo, "init") frontend = repo / "frontend" frontend.mkdir() (frontend / "app.txt").write_text("gated\n", encoding="utf-8") run_git(repo, "add", "frontend/app.txt") run_git(repo, "commit", "-m", "gated") first = run_git(repo, "rev-parse", "HEAD") docs = repo / "docs" docs.mkdir() (docs / "note.md").write_text("docs only\n", encoding="utf-8") run_git(repo, "add", "docs/note.md") run_git(repo, "commit", "-m", "docs") second = run_git(repo, "rev-parse", "HEAD") (frontend / "more.txt").write_text("more gated\n", encoding="utf-8") run_git(repo, "add", "frontend/more.txt") run_git(repo, "commit", "-m", "more gated") third = run_git(repo, "rev-parse", "HEAD") return {"repo": str(repo), "r": first, "h": second, "later": third} def ref_payload(branch: str, sha: str) -> list[dict]: return [{"ref": f"refs/heads/{branch}", "object": {"sha": sha}}] def gate_payload(sha: str, run_id: int = 42) -> dict: return { "workflow_runs": [ { "id": run_id, "path": ".gitea/workflows/backend-quality-gate.yml", "head_sha": sha, "head_branch": "staging", "event": "push", "conclusion": "success", } ] } def write_fixtures(directory: Path, manifest: list[dict], files: dict[str, object]) -> None: directory.mkdir() for name, payload in files.items(): (directory / name).write_text(json.dumps(payload), encoding="utf-8") (directory / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") def invoke(repo: str, fixture: Path, env_extra: dict[str, str]) -> subprocess.CompletedProcess[str]: env = os.environ.copy() # is-docs-only-range.sh calls python3. Windows Git bash has no python3, # and CreateProcess would rather hit the WSL stub than a PATH python. shim = fixture.parent / "python3-shim" shim.mkdir(exist_ok=True) launcher = shim / "python3" launcher.write_text( "#!/bin/bash\n" + f'exec "{PYTHON.as_posix()}" "$@"\n', encoding="utf-8", newline="\n", ) env.update( { "JYOTISHA_HTTP_FIXTURE_DIR": str(fixture), "JYOTISHA_DOCS_ONLY_MODE": "git", "JYOTISHA_PYTHON": str(PYTHON), "GATED_PATHS_FILE": str(GATED), "PATH": str(shim) + os.pathsep + env.get("PATH", ""), } ) env.update(env_extra) return subprocess.run( [BASH, str(SCRIPT)], cwd=repo, env=env, capture_output=True, text=True, encoding="utf-8", ) def outputs(completed: subprocess.CompletedProcess[str]) -> dict[str, str]: return dict(line.split("=", 1) for line in completed.stdout.splitlines() if "=" in line) def test_equal_heads_pass(tmp_path: Path) -> None: shas = make_repo(tmp_path) fixture = tmp_path / "equal" write_fixtures( fixture, [ {"match": "refs/heads/main", "file": "main.json"}, {"match": "refs/heads/staging", "file": "staging.json"}, {"match": "event=push", "file": "gate.json"}, ], { "main.json": ref_payload("main", shas["r"]), "staging.json": ref_payload("staging", shas["r"]), "gate.json": gate_payload(shas["r"]), }, ) completed = invoke(shas["repo"], fixture, {"REQUESTED_SHA": shas["r"]}) assert completed.returncode == 0, completed.stderr assert outputs(completed)["release_sha"] == shas["r"] assert outputs(completed)["head_sha"] == shas["r"] assert outputs(completed)["gate_run_id"] == "42" def test_docs_only_advance_passes(tmp_path: Path) -> None: shas = make_repo(tmp_path) fixture = tmp_path / "docs" write_fixtures( fixture, [ {"match": "refs/heads/main", "file": "main.json"}, {"match": "refs/heads/staging", "file": "staging.json"}, {"match": "event=push", "file": "gate.json"}, ], { "main.json": ref_payload("main", shas["h"]), "staging.json": ref_payload("staging", shas["h"]), "gate.json": gate_payload(shas["r"], 7), }, ) completed = invoke(shas["repo"], fixture, {"REQUESTED_SHA": shas["r"]}) assert completed.returncode == 0, completed.stderr assert outputs(completed)["release_sha"] == shas["r"] assert outputs(completed)["head_sha"] == shas["h"] assert outputs(completed)["gate_run_id"] == "7" def test_gated_advance_is_refused(tmp_path: Path) -> None: shas = make_repo(tmp_path) fixture = tmp_path / "gated" write_fixtures( fixture, [ {"match": "refs/heads/main", "file": "main.json"}, {"match": "refs/heads/staging", "file": "staging.json"}, {"match": "event=push", "file": "gate.json"}, ], { "main.json": ref_payload("main", shas["later"]), "staging.json": ref_payload("staging", shas["later"]), "gate.json": gate_payload(shas["h"]), }, ) completed = invoke(shas["repo"], fixture, {"REQUESTED_SHA": shas["h"]}) assert completed.returncode == 1 assert "not a docs-only ancestor" in completed.stderr def test_non_ancestor_is_refused(tmp_path: Path) -> None: shas = make_repo(tmp_path) fixture = tmp_path / "diverge" write_fixtures( fixture, [ {"match": "refs/heads/main", "file": "main.json"}, {"match": "refs/heads/staging", "file": "staging.json"}, {"match": "event=push", "file": "gate.json"}, ], { "main.json": ref_payload("main", shas["r"]), "staging.json": ref_payload("staging", shas["r"]), "gate.json": gate_payload(shas["later"]), }, ) completed = invoke(shas["repo"], fixture, {"REQUESTED_SHA": shas["later"]}) assert completed.returncode == 1 assert "not a docs-only ancestor" in completed.stderr def test_missing_gate_run_is_refused(tmp_path: Path) -> None: shas = make_repo(tmp_path) fixture = tmp_path / "nogate" write_fixtures( fixture, [ {"match": "refs/heads/main", "file": "main.json"}, {"match": "refs/heads/staging", "file": "staging.json"}, {"match": "event=push", "file": "gate.json"}, ], { "main.json": ref_payload("main", shas["r"]), "staging.json": ref_payload("staging", shas["r"]), "gate.json": {"workflow_runs": []}, }, ) completed = invoke(shas["repo"], fixture, {"REQUESTED_SHA": shas["r"]}) assert completed.returncode == 1 assert "no successful exact-SHA production quality gate run found" in completed.stderr def test_divergent_main_is_refused(tmp_path: Path) -> None: shas = make_repo(tmp_path) fixture = tmp_path / "split" write_fixtures( fixture, [ {"match": "refs/heads/main", "file": "main.json"}, {"match": "refs/heads/staging", "file": "staging.json"}, ], { "main.json": ref_payload("main", shas["r"]), "staging.json": ref_payload("staging", shas["h"]), }, ) completed = invoke(shas["repo"], fixture, {"REQUESTED_SHA": shas["r"]}) assert completed.returncode == 1 assert "main and staging must identify the same reviewed release" in completed.stderr def test_api_failure_exits_2(tmp_path: Path) -> None: shas = make_repo(tmp_path) fixture = tmp_path / "down" write_fixtures( fixture, [{"match": "refs/heads/staging", "file": "unused.json", "status": 500}], {"unused.json": {}}, ) completed = invoke(shas["repo"], fixture, {"REQUESTED_SHA": shas["r"]}) assert completed.returncode == 2 assert "Gitea API request failed" in completed.stderr