Files
Jyotisha/tests/test_resolve_release_revision.py
T
jesse-uxandGrok (agent loop) 32aa46b573 fix(release): close the first review of the production release path
Host rehearsal no longer needs Node on the production machine, and a failed cleanup now stops with the rehearsal database name. Docs-only staging heads no longer block rollback. Tests no longer hardcode Windows paths, and a trailing slash on the production site still refuses the test code.

pytest 12 passed; tsc exit 0; lint 0 errors; identity tests 9 passed, 0 cancelled. test:db was not run.

Co-Authored-By: Grok (agent loop)
2026-10-14 16:37:58 +08:00

271 lines
9.0 KiB
Python

"""Resolver for the production release SHA. Gitea responses are local fixtures."""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "deploy" / "resolve-release-revision.sh"
GATED = ROOT / "deploy" / "gated-paths.txt"
PYTHON = Path(sys.executable)
def resolve_bash() -> str:
found = shutil.which("bash")
git_bash = Path(r"D:\Program Files\Git\bin\bash.exe")
if os.name == "nt" and git_bash.exists():
lowered = (found or "").lower()
if not found or "system32" in lowered or "windowsapps" in lowered:
return str(git_bash)
if found:
return found
if git_bash.exists():
return str(git_bash)
return "bash"
BASH = resolve_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