modify harness framework
This commit is contained in:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user