mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
- Move utils/gha/annotations.py → utils/annotations/message.py - Move scripts/gha/summarize_annotations.py → utils/annotations/summarize.py - Keep utils/gha/annotations.py as compatibility shim - Update all imports across codebase to utils.annotations.message - Wire tee + summary step into test-code-lint, unit, and integration workflows - Add unit tests for utils/annotations/summarize (20 tests) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
101 lines
2.5 KiB
Python
101 lines
2.5 KiB
Python
"""GitHub Actions workflow command annotations.
|
|
|
|
Outputs structured log messages that GitHub Actions renders as annotations
|
|
(warning triangles, error markers, etc.) in the job summary and log view.
|
|
|
|
See: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-warning-message
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Iterable, Optional
|
|
|
|
|
|
def in_github_actions() -> bool:
|
|
return os.environ.get("GITHUB_ACTIONS") == "true"
|
|
|
|
|
|
def _emit(
|
|
level: str,
|
|
message: str,
|
|
*,
|
|
title: Optional[str] = None,
|
|
file: Optional[str] = None,
|
|
line: Optional[int] = None,
|
|
col: Optional[int] = None,
|
|
) -> None:
|
|
if in_github_actions():
|
|
parts = []
|
|
if title:
|
|
parts.append(f"title={title}")
|
|
if file:
|
|
parts.append(f"file={file}")
|
|
if line is not None:
|
|
parts.append(f"line={line}")
|
|
if col is not None:
|
|
parts.append(f"col={col}")
|
|
props = ",".join(parts)
|
|
prefix = f"::{level} {props}::" if props else f"::{level}::"
|
|
print(f"{prefix}{message}", flush=True)
|
|
else:
|
|
label = f"[{level.upper()}]"
|
|
if title:
|
|
label += f" {title}"
|
|
print(f"{label}: {message}", flush=True)
|
|
|
|
|
|
def warning(
|
|
message: str,
|
|
*,
|
|
title: Optional[str] = None,
|
|
file: Optional[str] = None,
|
|
line: Optional[int] = None,
|
|
col: Optional[int] = None,
|
|
) -> None:
|
|
_emit("warning", message, title=title, file=file, line=line, col=col)
|
|
|
|
|
|
def error(
|
|
message: str,
|
|
*,
|
|
title: Optional[str] = None,
|
|
file: Optional[str] = None,
|
|
line: Optional[int] = None,
|
|
col: Optional[int] = None,
|
|
) -> None:
|
|
_emit("error", message, title=title, file=file, line=line, col=col)
|
|
|
|
|
|
def notice(
|
|
message: str,
|
|
*,
|
|
title: Optional[str] = None,
|
|
file: Optional[str] = None,
|
|
line: Optional[int] = None,
|
|
col: Optional[int] = None,
|
|
) -> None:
|
|
_emit("notice", message, title=title, file=file, line=line, col=col)
|
|
|
|
|
|
def warning_each(
|
|
items: Iterable[str],
|
|
*,
|
|
title: Optional[str] = None,
|
|
file: Optional[str] = None,
|
|
) -> None:
|
|
"""Emit one warning annotation per item in *items*."""
|
|
for item in items:
|
|
warning(item, title=title, file=file)
|
|
|
|
|
|
def error_each(
|
|
items: Iterable[str],
|
|
*,
|
|
title: Optional[str] = None,
|
|
file: Optional[str] = None,
|
|
) -> None:
|
|
"""Emit one error annotation per item in *items*."""
|
|
for item in items:
|
|
error(item, title=title, file=file)
|