"""Safe single-key YAML frontmatter reader (ORCH-016 / ADR-001 §5). The status-comment builder (build_status_comment) needs to surface verdict / deploy_status / staging_status from the per-stage artifact files (12-review.md, 13-test-report.md, 14-deploy-log.md, 15-staging-log.md). Those files share the same leading-YAML-frontmatter convention used by the quality gates — but the comment hot-path must NEVER raise: a missing file, malformed YAML, or absent key should simply suppress the verdict line, not break the run. This module is a tiny defensive helper: - `read_frontmatter_value(path, key)` -> str | None - swallows every exception, logs to logger.debug, returns None. It intentionally duplicates ~10 lines of YAML-frontmatter logic that already exist in `src/qg/checks.py` (S-5 / БАГ 8 / ET-013 fixes). ADR-001 §5 accepts this duplication to keep the blast radius of ORCH-016 small (no QG refactor in this PR); merging into a single parser is a follow-up task. """ import logging logger = logging.getLogger("orchestrator.frontmatter") def read_frontmatter_value(path: str, key: str) -> str | None: """Return the value of `key` from the leading YAML frontmatter of `path`. Format expected (canonical, matching qg/checks.py): --- key: value other: ... --- Never raises. Returns None for any of: - missing/unreadable file, - no leading `---` frontmatter, - malformed/unterminated frontmatter, - YAML parse error, - frontmatter is not a mapping, - key absent (or its value is None/empty). The returned value is stringified and stripped (whitespace removed); casing is preserved so the caller decides whether to upper/lower for matching. """ try: with open(path, "r", encoding="utf-8", errors="replace") as f: content = f.read() except OSError as e: logger.debug(f"read_frontmatter_value: cannot open {path}: {e}") return None if not content.startswith("---"): return None parts = content.split("---", 2) if len(parts) < 3: # Unterminated frontmatter. return None try: import yaml fm = yaml.safe_load(parts[1]) or {} except Exception as e: # yaml.YAMLError + anything pyyaml may surface logger.debug(f"read_frontmatter_value: yaml parse failed for {path}: {e}") return None if not isinstance(fm, dict): return None raw = fm.get(key) if raw is None: return None value = str(raw).strip() return value or None