utils/todo_hooks/checkrun.py
Krzysztof kuhy Rudnicki aec96eefd3 Add todo_hooks closer, drift sweep, PR import
Python 3.14 stdlib package wiring git to the todo MCP server:
- closer (post-commit): runs the done_check of each Todo: trailer
  task and close_task's it only on exit 0. It FAILS OPEN: the commit
  already happened, so a down server, bad token or no gh prints one
  warning and exits 0 instead of failing the hook.
- sweep: nightly drift report (systemd units shipped, not enabled).
- import-pr: a merged PR's '## Todo:' items become tasks, deduped.
- A done_check runs only if its sha256 is in
  ~/.config/todo-hooks/trusted-checks; 'Check: manual' notes are
  never auto-closed.
- Hooks exposed in .pre-commit-hooks.yaml (post-commit, post-merge,
  post-rewrite).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 12:24:29 +01:00

63 lines
1.6 KiB
Python

"""Run a done_check with a hard timeout, in its own process group."""
from __future__ import annotations
from dataclasses import dataclass
import os
import signal
import subprocess
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pathlib import Path
CHECK_TIMEOUT = 55.0
@dataclass(frozen=True)
class CheckResult:
"""Exit code, or None on timeout or failure to start."""
code: int | None
timed_out: bool
@property
def passed(self) -> bool:
return self.code == 0
def describe(self) -> str:
if self.timed_out:
return f"timed out after {CHECK_TIMEOUT:g}s"
return "could not start" if self.code is None else f"exit {self.code}"
def run_check(
command: str,
cwd: Path,
*,
timeout: float = CHECK_TIMEOUT,
wrapper: Path | None = None,
) -> CheckResult:
"""``bash -c command`` from `cwd` without GIT_* env; kill the group on timeout."""
argv = [str(wrapper)] if wrapper else []
argv += ["bash", "-c", command]
env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
try:
proc = subprocess.Popen(
argv,
cwd=cwd,
env=env,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
except OSError:
return CheckResult(None, timed_out=False)
try:
return CheckResult(proc.wait(timeout=timeout), timed_out=False)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL)
proc.wait()
return CheckResult(None, timed_out=True)