mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
A publication run sat silent for 8+ minutes while it downloaded ~20 GB, with no way to tell which phase it was in or why a claim was held. scripts/releases/progress.py logs to stderr (stdout stays the JSON steps) with GitHub ::group::/::notice::/::error:: markers: - every git/gh/docker command the sequencer runs, with its time and result - discovery counts, and each claim's state with the reason (run attempt and conclusion, draft location, abandon marker, receipt/release match) - the plan: why each version advances, is skipped, or is held - each step as a timed group: publish (receipt digests) and advance - the channel advance's phases, plus per-file progress for the handoff fetch and candidate materialize downloads (the slow part) - the head before and after, and a top-level ::error:: on failure
75 lines
2.5 KiB
Python
75 lines
2.5 KiB
Python
"""Progress logging for release automation.
|
|
|
|
Everything goes to stderr, flushed, so stdout stays the machine-readable
|
|
result (the sequencer prints its steps as JSON there). On GitHub Actions the
|
|
``::group::`` / ``::notice::`` lines render as collapsible sections and
|
|
annotations; locally they read as plain prefixed lines.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import sys
|
|
import time
|
|
from collections.abc import Callable, Iterator
|
|
|
|
# Long argv values (release bodies, JSON payloads) are cut in command logs.
|
|
_ARG_LIMIT = 160
|
|
|
|
|
|
def log(message: str) -> None:
|
|
print(message, file=sys.stderr, flush=True)
|
|
|
|
|
|
def notice(message: str, *, title: str | None = None) -> None:
|
|
log(f"::notice{f' title={title}' if title else ''}::{message}")
|
|
|
|
|
|
def error(message: str, *, title: str | None = None) -> None:
|
|
log(f"::error{f' title={title}' if title else ''}::{message}")
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def group(title: str) -> Iterator[None]:
|
|
"""A collapsible section that reports how long it took and whether it failed."""
|
|
log(f"::group::{title}")
|
|
started = time.monotonic()
|
|
try:
|
|
yield
|
|
except BaseException as exc:
|
|
log(f"✗ {title} failed after {time.monotonic() - started:.1f}s: {type(exc).__name__}: {exc}")
|
|
log("::endgroup::")
|
|
raise
|
|
log(f"✓ {title} ({time.monotonic() - started:.1f}s)")
|
|
log("::endgroup::")
|
|
|
|
|
|
def describe_command(argv: list[str]) -> str:
|
|
shown = []
|
|
for arg in argv:
|
|
text = str(arg)
|
|
if len(text) > _ARG_LIMIT:
|
|
text = f"{text[:_ARG_LIMIT]}…(+{len(text) - _ARG_LIMIT} chars)"
|
|
shown.append(text.replace("\n", "\\n"))
|
|
return " ".join(shown)
|
|
|
|
|
|
def logged_run(run: Callable[[list[str]], str]) -> Callable[[list[str]], str]:
|
|
"""Wrap a ``run(argv) -> str`` so every command logs, with its time and outcome."""
|
|
if getattr(run, "_progress_logged", False):
|
|
return run
|
|
|
|
def wrapper(argv: list[str]) -> str:
|
|
log(f" $ {describe_command(argv)}")
|
|
started = time.monotonic()
|
|
try:
|
|
result = run(argv)
|
|
except BaseException as exc:
|
|
log(f" ↳ failed after {time.monotonic() - started:.1f}s: {type(exc).__name__}: {exc}")
|
|
raise
|
|
lines = result.count("\n") + 1 if result else 0
|
|
log(f" ↳ ok in {time.monotonic() - started:.1f}s ({lines} line{'s' if lines != 1 else ''})")
|
|
return result
|
|
|
|
wrapper._progress_logged = True # type: ignore[attr-defined]
|
|
return wrapper
|