Files
Jyotisha/tests/test_resolve_release_revision.py
T
jesse-uxandGrok (agent loop) 00e6efdbfa 发布可以跟上只多了文档的测试环境头
四个生产工作流改用同一条发布修订:最近一次代码门禁成功的提交可以发布,后面只多文档不再挡住。生产环境拒绝测试验证码。63 个还没上生产的迁移写了分级、演练脚本和首发步骤。没有点任何生产按钮。

自测:发布修订 7 通过,演练安全 3 通过,tsc 0 错,lint 0 error。本机空库演练在服务角色读人物表处按设计停止(退出码 4,其他人物已确认行数为 0)。

Co-Authored-By: Grok (agent loop)
2026-10-14 10:24:40 +08:00

254 lines
8.6 KiB
Python

"""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