Harness governance / validate (push) Has been cancelled
Initialize the full harness coding document set from the harness_coding_docs template, customized for the SoftBox project: - Vision, requirements, tech stack (modern Go 1.25 + Gio v0.10.1; Win7 legacy Go 1.20.14 + Gio v0.6.0), architecture, coding rules - Protocol contracts (signed catalog, package protocol v1, Ed25519 license, events, CLI) and Gio view structure - Roadmap Phase 0-6 with 20 suggested tasks; T-001 (monorepo skeleton) filed and ready to claim - Agent entry points (AGENTS.md, docs/00-ai-start-here.md), context manifest, governance scripts and tests - Merge Go gitignore with harness rules; keep go.work tracked Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
167 lines
6.0 KiB
Python
167 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Compatibility smoke for Gitea's same-name claim branch race behavior."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
import threading
|
|
import urllib.parse
|
|
import uuid
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from setup_gitea_labels import ApiError, GiteaClient, validate_config
|
|
|
|
|
|
PROBE_PREFIX = "claims/__probe__/race-"
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="并发创建唯一临时 claim 分支,smoke 期望一个 201、一个 409。"
|
|
)
|
|
parser.add_argument("--repo", help="目标 owner/repo;也可设置 GITEA_REPOSITORY。")
|
|
parser.add_argument(
|
|
"--apply",
|
|
action="store_true",
|
|
help="执行两次写入并清理临时分支;省略时只读并打印计划。",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def new_probe_branch() -> str:
|
|
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
return f"{PROBE_PREFIX}{stamp}-{uuid.uuid4().hex[:12]}"
|
|
|
|
|
|
def branch_commit(client: GiteaClient, branch: str) -> str | None:
|
|
encoded = urllib.parse.quote(branch, safe="")
|
|
try:
|
|
response = client.request("GET", f"/branches/{encoded}")
|
|
except ApiError as exc:
|
|
if exc.status == 404:
|
|
return None
|
|
raise
|
|
if not isinstance(response, dict):
|
|
raise RuntimeError("Gitea branch 响应格式异常。")
|
|
commit = response.get("commit")
|
|
if not isinstance(commit, dict) or not isinstance(commit.get("id"), str):
|
|
raise RuntimeError("Gitea branch 响应缺少 commit.id。")
|
|
return commit["id"]
|
|
|
|
|
|
def repository_base(client: GiteaClient) -> tuple[str, str]:
|
|
repository = client.request("GET", "")
|
|
if not isinstance(repository, dict) or not isinstance(
|
|
repository.get("default_branch"), str
|
|
):
|
|
raise RuntimeError("Gitea repository 响应缺少 default_branch。")
|
|
default_branch = repository["default_branch"]
|
|
commit = branch_commit(client, default_branch)
|
|
if commit is None:
|
|
raise RuntimeError("默认分支不存在。")
|
|
return default_branch, commit
|
|
|
|
|
|
def create_once(client: GiteaClient, barrier: threading.Barrier, branch: str, ref: str) -> int:
|
|
barrier.wait(timeout=10)
|
|
try:
|
|
client.request(
|
|
"POST",
|
|
"/branches",
|
|
{"new_branch_name": branch, "old_ref_name": ref},
|
|
)
|
|
return 201
|
|
except ApiError as exc:
|
|
return exc.status
|
|
|
|
|
|
def cleanup_probe(client: GiteaClient, branch: str, expected_sha: str) -> None:
|
|
if not branch.startswith(PROBE_PREFIX):
|
|
raise RuntimeError("拒绝清理非探针分支。")
|
|
actual_sha = branch_commit(client, branch)
|
|
if actual_sha is None:
|
|
return
|
|
if actual_sha != expected_sha:
|
|
raise RuntimeError("探针分支 SHA 与预期不一致,已保留供人工检查。")
|
|
encoded = urllib.parse.quote(branch, safe="")
|
|
client.request("DELETE", f"/branches/{encoded}")
|
|
if branch_commit(client, branch) is not None:
|
|
raise RuntimeError("探针分支清理后仍然存在。")
|
|
|
|
|
|
def client_for(root: str, owner: str, repo: str, token: str) -> GiteaClient:
|
|
return GiteaClient(root, owner, repo, token)
|
|
|
|
|
|
def run_probe(root: str, owner: str, repo: str, token: str, branch: str, sha: str) -> list[int]:
|
|
barrier = threading.Barrier(2)
|
|
clients = [client_for(root, owner, repo, token) for _ in range(2)]
|
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
futures = [
|
|
executor.submit(create_once, client, barrier, branch, sha) for client in clients
|
|
]
|
|
return sorted(future.result(timeout=40) for future in futures)
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
repo_value = args.repo or os.environ.get("GITEA_REPOSITORY")
|
|
if not repo_value:
|
|
print("ERROR: 需要 --repo owner/repo 或 GITEA_REPOSITORY。", file=sys.stderr)
|
|
return 2
|
|
token = os.environ.get("GITEA_TOKEN", "")
|
|
try:
|
|
root, owner, repo = validate_config(
|
|
os.environ.get("GITEA_URL", ""), token, repo_value
|
|
)
|
|
control = client_for(root, owner, repo, token)
|
|
default_branch, sha = repository_base(control)
|
|
branch = new_probe_branch()
|
|
print(f"目标仓库:{repo_value}")
|
|
print(f"基准分支:{default_branch} @ {sha}")
|
|
print(f"临时分支:{branch}")
|
|
if not args.apply:
|
|
print("dry-run:未写入;追加 --apply 才会执行竞态探针和受控清理。")
|
|
return 0
|
|
|
|
results: list[int] = []
|
|
probe_error: Exception | None = None
|
|
try:
|
|
results = run_probe(root, owner, repo, token, branch, sha)
|
|
except Exception as exc: # cleanup still has to run after partial writes
|
|
probe_error = exc
|
|
try:
|
|
cleanup_probe(control, branch, sha)
|
|
except (ApiError, RuntimeError) as cleanup_error:
|
|
print(f"ERROR: 清理失败:{cleanup_error}", file=sys.stderr)
|
|
return 2
|
|
if probe_error is not None:
|
|
print("ERROR: 竞态请求未完整返回;临时分支已安全清理。", file=sys.stderr)
|
|
return 2
|
|
print("竞态结果:" + ", ".join(str(status) for status in results))
|
|
if results != [201, 409]:
|
|
print(
|
|
"ERROR: 未得到恰好一个 201 和一个 409;目标实例不符合预期 smoke,临时分支已安全清理。",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
print("claim 并发兼容性 smoke 通过;这不证明线性化,临时分支已删除并确认 404。")
|
|
return 0
|
|
except ValueError as exc:
|
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
return 2
|
|
except ApiError as exc:
|
|
print(f"ERROR: Gitea 探针失败(HTTP {exc.status})。", file=sys.stderr)
|
|
return 2
|
|
except RuntimeError as exc:
|
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|