Files

142 lines
5.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""按分支改动选择最小直接检查:host 实施中的 make 目标子集(完整套件仍以 CI 为准)。
用法:test_affected.py [--base REF] [--dry-run]
base:--base,否则环境变量 BASE,否则当前分支卡片的 immediate_target(task_pr_check 助手),
再否则 origin/main;裸分支名在 origin/<name> 存在时解析为 origin/<name>。
改动路径 = `git diff <merge-base>...HEAD` ∪ 未提交的已跟踪改动(相对 HEAD)。路径到目标:
scripts/** tests/** .gitea/** policy/** Makefile AGENTS.md AGENT-EXECUTION.md PROJECT.md → test-governance
modules/{core,backend,frontend,ui} → test-{core,backend,frontend,ui};deps/agent-command → test-agentcmd;
deps/agent-{intent,native,squad} → test-libs;gitlink 本身或 .gitmodules → test-pins;
modules/** 或 scripts/lint-boundaries.sh → test-boundaries;其余(.task/**、docs/**、其他 *.md)不选。
选中任一目标时先跑 test-worktree;单次 `make <targets>` 执行,退出码随 make。
退出码:make 的退出码;无目标或 --dry-run 为 0;用法/ref 错误为 2。仅使用标准库。
"""
import argparse
import os
import subprocess
import sys
DEFAULT_BASE = "origin/main"
EXIT_USAGE = 2
GITLINK_MODE = "160000"
ORDER = ("test-governance", "test-boundaries", "test-pins", "test-libs", "test-core",
"test-backend", "test-frontend", "test-ui", "test-agentcmd")
GOVERNANCE_DIRS = ("scripts/", "tests/", ".gitea/", "policy/")
GOVERNANCE_FILES = ("Makefile", "AGENTS.md", "AGENT-EXECUTION.md", "PROJECT.md")
COMPONENTS = {
"modules/core": "test-core",
"modules/backend": "test-backend",
"modules/frontend": "test-frontend",
"modules/ui": "test-ui",
"deps/agent-command": "test-agentcmd",
"deps/agent-intent": "test-libs",
"deps/agent-native": "test-libs",
"deps/agent-squad": "test-libs",
}
class Stop(Exception):
pass
def git(*args):
proc = subprocess.run(["git", *args], capture_output=True, text=True, encoding="utf-8", errors="replace")
if proc.returncode != 0:
raise Stop(f"git {' '.join(args)} failed: {proc.stderr.strip() or proc.returncode}")
return proc.stdout
def ref_exists(ref):
return subprocess.run(["git", "rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}"],
capture_output=True).returncode == 0
def card_target():
"""当前分支卡片的 immediate_target;无分支/无助手/无卡时返回 None。"""
try:
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import task_pr_check
meta, text = task_pr_check.branch_meta(task_pr_check.current_branch(), "main")
except Exception:
return None
return meta.get("immediate_target") if text is not None else None
def resolve_base(requested):
name = (requested or os.environ.get("BASE", "")).strip() or card_target() or DEFAULT_BASE
if not name.startswith("origin/") and ref_exists(f"refs/remotes/origin/{name}"):
name = f"origin/{name}"
if not ref_exists(name):
raise Stop(f"base {name} does not resolve to a commit")
return name
def raw_changes(*diff_args):
"""{path: is_gitlink},取自 `git diff --raw -z --no-renames`。"""
out = git("diff", "--raw", "-z", "--no-renames", *diff_args)
fields = out.split("\0")
changes = {}
for meta, path in zip(fields[0::2], fields[1::2]):
modes = meta.lstrip(":").split()[:2]
changes[path] = changes.get(path, False) or GITLINK_MODE in modes
return changes
def changed_paths(base):
merge_base = git("merge-base", base, "HEAD").strip()
changes = raw_changes(merge_base, "HEAD")
for path, gitlink in raw_changes("HEAD").items():
changes[path] = changes.get(path, False) or gitlink
return changes
def targets_for(path, gitlink):
found = set()
if path.startswith(GOVERNANCE_DIRS) or path in GOVERNANCE_FILES:
found.add("test-governance")
for prefix, target in COMPONENTS.items():
if path == prefix or path.startswith(prefix + "/"):
found.add(target)
if gitlink or path == ".gitmodules":
found.add("test-pins")
if path.startswith("modules/") or path == "scripts/lint-boundaries.sh":
found.add("test-boundaries")
return found
def select(changes):
found = set()
for path, gitlink in changes.items():
found |= targets_for(path, gitlink)
return ["test-worktree", *(t for t in ORDER if t in found)] if found else []
def main(argv=None):
parser = argparse.ArgumentParser(prog="test_affected.py", description="按分支改动运行最小直接检查")
parser.add_argument("--base")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args(argv)
try:
os.chdir(git("rev-parse", "--show-toplevel").strip())
base = resolve_base(args.base)
changes = changed_paths(base)
except Stop as stop:
print(f"[affected] ERROR {stop}")
return EXIT_USAGE
targets = select(changes)
print(f"[affected] base={base} changed={len(changes)} targets={','.join(targets) or 'none'}", flush=True)
if not targets:
print("[affected] nothing to test")
return 0
if args.dry_run:
print(f"[affected] dry-run: make {' '.join(targets)}")
return 0
return subprocess.run(["make", *targets]).returncode
if __name__ == "__main__":
sys.exit(main())