Files
TheRock/build_tools/github_actions/detect_failed_logs.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

219 lines
6.2 KiB
Python

#!/usr/bin/env python3
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""
Detects failed teatime build logs and creates a small 0.error.*.log file
that contains only the important failure context.
The original log is preserved unchanged.
Log directory resolution order:
1. --log-dir
2. OUTPUT_DIR/build/logs on Linux CI
3. BUILD_DIR/logs as fallback
4. build/logs as the final default
Summary output resolution order:
1. --summary-path
2. GITHUB_STEP_SUMMARY
3. skip summary generation if neither is available
This script can also be run manually:
python build_tools/github_actions/detect_failed_logs.py \
--log-dir build/logs \
--summary-path /tmp/summary.md
"""
from __future__ import annotations
import argparse
import os
import re
import sys
from pathlib import Path
IMPORTANT_RE = re.compile(
r"(FAILED:|error:|CMake Error|Traceback|FileNotFoundError|ninja: build stopped|subcommand failed)",
)
# teatime END records are tab-separated.
# We only care that the line starts with END and that the last field is a
# non-zero exit code, so the parser is resilient if teatime adds fields later.
def is_failed_end_line(line: str) -> bool:
fields = line.rstrip("\n").split("\t")
if len(fields) < 4:
return False
if fields[0] != "END":
return False
try:
return int(fields[-1]) != 0
except ValueError:
return False
def get_failed_end_line(path: Path, tail_bytes: int = 4096) -> str | None:
"""Read only the tail of the log when searching for a failed END record.
Teatime writes the END line at the end of the log, so scanning the last
few KiB avoids reading large log files into memory.
"""
try:
with path.open("rb") as f:
f.seek(0, os.SEEK_END)
size = f.tell()
f.seek(max(0, size - tail_bytes))
tail = f.read().decode("utf-8", errors="replace")
except OSError as e:
print(f"Warning: failed to read log {path}: {e}", file=sys.stderr)
return None
for line in reversed(tail.splitlines()):
if is_failed_end_line(line):
return line
return None
def find_failed_logs(log_dir: Path, tail_bytes: int = 4096) -> list[tuple[Path, str]]:
failed: list[tuple[Path, str]] = []
for path in sorted(log_dir.glob("*.log")):
if path.name.startswith("0.error."):
continue
failure_end = get_failed_end_line(path, tail_bytes=tail_bytes)
if failure_end:
failed.append((path, failure_end))
return failed
def build_excerpt(
lines: list[str],
window_before: int = 12,
window_after: int = 20,
) -> list[str]:
"""
Return a small, deterministic excerpt centered around the most recent
important failure line. Falls back to the tail of the log if nothing matches.
"""
important_idx = None
for i in range(len(lines) - 1, -1, -1):
if IMPORTANT_RE.search(lines[i]):
important_idx = i
break
if important_idx is None:
excerpt = lines[-(window_before + window_after) :]
else:
start = max(0, important_idx - window_before)
end = min(len(lines), important_idx + window_after + 1)
excerpt = lines[start:end]
return excerpt
def write_failure_summary_log(
src: Path,
dst: Path,
failure_end: str,
) -> None:
lines = src.read_text(encoding="utf-8", errors="replace").splitlines()
excerpt = build_excerpt(lines)
dst.parent.mkdir(parents=True, exist_ok=True)
with dst.open("w", encoding="utf-8") as out:
out.write(f"Failure summary for: {src.name}\n")
out.write(f"Source log: {src}\n")
out.write("\n")
out.write("Failed END line:\n")
out.write(f"{failure_end}\n\n")
out.write("Important excerpt:\n")
out.write("\n".join(excerpt))
out.write("\n\n")
out.write(f"See original log: {src.name}\n")
def resolve_logs_dir(args: argparse.Namespace) -> Path:
if args.log_dir:
return args.log_dir
output_dir = os.environ.get("OUTPUT_DIR")
if output_dir:
return Path(output_dir) / "build" / "logs"
build_dir = os.environ.get("BUILD_DIR", "build")
return Path(build_dir) / "logs"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Detect failed teatime logs and generate 0.error.*.log summaries."
)
parser.add_argument(
"--log-dir",
type=Path,
help="Directory containing *.log files. Overrides env-based fallback.",
)
parser.add_argument(
"--summary-path",
type=Path,
help="GitHub step summary file path. Overrides GITHUB_STEP_SUMMARY.",
)
parser.add_argument(
"--tail-bytes",
type=int,
default=4096,
help="Number of bytes to read from the end of each log when searching for END.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
logs_dir = resolve_logs_dir(args)
if not logs_dir.exists():
print(f"Log directory does not exist: {logs_dir}", file=sys.stderr)
return 0
summary_path = args.summary_path or os.environ.get("GITHUB_STEP_SUMMARY")
if not summary_path:
print("GITHUB_STEP_SUMMARY is not set; skipping failed log summary generation.")
return 0
summary_path = Path(summary_path)
failed_logs = find_failed_logs(logs_dir, tail_bytes=args.tail_bytes)
if not failed_logs:
print("No failed log found.")
return 0
with summary_path.open("a", encoding="utf-8") as summary:
summary.write("## Build failure\n")
summary.write(f"**Error logs:** {len(failed_logs)}\n")
summary.write("\n")
for src, failure_end in failed_logs:
dst = src.with_name(f"0.error.{src.name}")
try:
write_failure_summary_log(src, dst, failure_end)
print(f"Created {dst.name} from {src.name}")
summary.write(f"- `{dst.name}`\n")
except OSError as e:
print(
f"Failed to create failure summary log for {src}: {e}",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())