modify harness framework

This commit is contained in:
김경종
2026-09-08 10:30:07 +09:00
parent 1239d5b9f6
commit 1a0f3c9aaf
68 changed files with 6729 additions and 957 deletions
@@ -3,10 +3,11 @@
import json
import os
import re
import subprocess
import sys
import time
from pathlib import Path
from pathlib import Path, PurePosixPath, PureWindowsPath
SCRIPTS = Path(__file__).resolve().parents[1]
@@ -48,6 +49,7 @@ def _project_root(
cwd=resolved_cwd,
capture_output=True,
text=True,
encoding="utf-8",
shell=False,
timeout=_remaining(deadline, clock, "repository discovery"),
)
@@ -61,6 +63,110 @@ def _project_root(
return resolved_cwd
def _changed_path_kind(root: Path, phase: Path, raw: str) -> str | None:
if (not raw or raw.endswith("/") or "\\" in raw
or PureWindowsPath(raw).drive or PureWindowsPath(raw).root
or ".." in raw.split("/")):
return None
path = root / raw
if path.exists() and not path.is_file():
return None
resolved = path.resolve()
# Symlinks/junctions can make an apparent document modify a build input.
if not resolved.is_relative_to(root) or resolved != path:
return None
relative = resolved.relative_to(root)
if relative == Path("AGENTS.md"):
return "doc"
if relative.parts and relative.parts[0] == "docs" and relative.suffix.lower() == ".md":
return "doc"
if relative.parent == phase and (relative.name == "index.json"
or re.fullmatch(r"step\d+-output\.json", relative.name)):
return "metadata"
return None
def _was_regular_file(root: Path, raw: str, *, deadline: float, run, clock) -> bool:
"""Missing paths may be deleted gitlinks rather than deleted Markdown files."""
for args in (("ls-files", "--stage", "-z", "--", raw),
("ls-tree", "-z", "HEAD", "--", raw)):
result = run(
["git", *args], cwd=root, capture_output=True, text=True,
encoding="utf-8", errors="strict", shell=False,
timeout=_remaining(deadline, clock, "deleted document type detection"),
)
if result.returncode != 0:
return False
if not result.stdout:
continue
records = result.stdout.split("\0")
if len(records) != 2 or records[1] or "\t" not in records[0]:
return False
info, name = records[0].split("\t", 1)
fields = info.split()
return name == raw and len(fields) == 3 and fields[0] in {"100644", "100755"}
return False
def _documentation_only_changes(
root: Path,
phase_dir: str,
*,
deadline: float,
run=subprocess.run,
clock=time.monotonic,
) -> bool:
"""Opt-in plus an unambiguous, document-only Git diff permits skipping MSVC."""
if not phase_dir:
return False
parts = PurePosixPath(phase_dir).parts
if (len(parts) != 2 or parts[0] != "phases" or parts[1] in {".", ".."}
or "\\" in phase_dir or ":" in phase_dir):
return False
root = root.resolve()
phase = Path(*parts)
try:
if (root / phase).resolve() != root / phase:
return False
result = run(
["git", "status", "--porcelain=v1", "-z", "--untracked-files=all"],
cwd=root,
capture_output=True,
text=True,
encoding="utf-8",
errors="strict",
shell=False,
timeout=_remaining(deadline, clock, "documentation change detection"),
)
if result.returncode != 0 or not result.stdout or not result.stdout.endswith("\0"):
return False
records = iter(result.stdout[:-1].split("\0"))
has_document = False
for record in records:
if len(record) < 4 or record[2] != " ":
return False
status = record[:2]
if status != "??" and (any(c not in " MARCD" for c in status)
or status in {" ", "AA", "DD"}):
return False
paths = [record[3:]]
if "R" in status or "C" in status:
paths.append(next(records)) # -z uses destination, then original path.
for raw in paths:
kind = _changed_path_kind(root, phase, raw)
if kind is None:
return False
if not (root / raw).exists() and not _was_regular_file(
root, raw, deadline=deadline, run=run, clock=clock,
):
return False
has_document |= kind == "doc"
return has_document
except (OSError, UnicodeError, ValueError, StopIteration,
subprocess.TimeoutExpired, ValidationFailure):
return False
def run_validations(
root: Path,
*,
@@ -133,6 +239,11 @@ def main(*, run=subprocess.run, clock=time.monotonic) -> int:
run=run,
clock=clock,
)
if _documentation_only_changes(
root, os.environ.get("HARNESS_DOCS_ONLY_PHASE", ""),
deadline=deadline, run=run, clock=clock,
):
return 0
ok, message = run_validations(root, deadline=deadline, clock=clock)
except (OSError, ValidationFailure) as exc:
_emit_stop_response(f"validation hook failed: {exc}")