Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions src/github_repo_auditor/portfolio_truth_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,18 +566,8 @@ def _publish_portfolio_truth_locked(
build_result.snapshot, latest_name
)

with tempfile.NamedTemporaryFile(
"w", delete=False, dir=output_dir, suffix=".registry-check.md"
) as handle:
temp_registry_path = Path(handle.name)
try:
validate_registry_markdown(
registry_markdown, build_result.snapshot, temp_registry_path
)
validate_portfolio_report_markdown(report_markdown)
finally:
if temp_registry_path.exists():
temp_registry_path.unlink()
validate_registry_markdown(registry_markdown, build_result.snapshot)
validate_portfolio_report_markdown(report_markdown)

targets = {
snapshot_path: snapshot_json,
Expand Down
11 changes: 3 additions & 8 deletions src/github_repo_auditor/portfolio_truth_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
SecurityFields,
)
from github_repo_auditor.producer_preflight import ProducerEvidence
from github_repo_auditor.registry_parser import _normalize, parse_registry
from github_repo_auditor.registry_parser import _normalize, parse_registry_markdown
from github_repo_auditor.security_admission import derive_security_admission


Expand Down Expand Up @@ -2323,14 +2323,9 @@ def validate_publish_targets(


def validate_registry_markdown(
markdown: str, snapshot: PortfolioTruthSnapshot, temp_path: Path
markdown: str, snapshot: PortfolioTruthSnapshot, temp_path: Path | None = None
) -> None:
temp_path.write_text(markdown)
try:
parsed = parse_registry(temp_path)
finally:
if temp_path.exists():
temp_path.unlink()
parsed = parse_registry_markdown(markdown)
expected_labels = registry_project_labels(snapshot.projects).values()
expected = {_normalize(label.strip()) for label in expected_labels}
parsed_names = {_normalize(name) for name in parsed}
Expand Down
10 changes: 7 additions & 3 deletions src/github_repo_auditor/registry_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,12 @@ def _normalize(name: str) -> str:
return n


def parse_registry(path: Path) -> dict[str, str]:
"""Parse project-registry.md into {project_name: status} mapping.
def parse_registry_markdown(content: str) -> dict[str, str]:
"""Parse registry markdown into {project_name: status} mapping.

Handles variable-column markdown tables across multiple sections.
Column 1 is always Project, Column 2 is always Status.
"""
content = path.read_text(errors="replace")
projects: dict[str, str] = {}

for line in content.splitlines():
Expand Down Expand Up @@ -76,6 +75,11 @@ def parse_registry(path: Path) -> dict[str, str]:
return projects


def parse_registry(path: Path) -> dict[str, str]:
"""Parse project-registry.md into {project_name: status} mapping."""
return parse_registry_markdown(path.read_text(errors="replace"))


@dataclass
class RegistryReconciliation:
on_github_not_registry: list[str]
Expand Down
21 changes: 21 additions & 0 deletions tests/test_portfolio_truth.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
_same_repository_path,
canonicalize_prior_security_truth_payload,
validate_portfolio_report_markdown,
validate_registry_markdown,
validate_truth_snapshot,
)
from github_repo_auditor.project_registry import build_project_registry
Expand Down Expand Up @@ -4766,6 +4767,26 @@ def test_rendered_registry_round_trips_through_parser(
assert "## Cowork Task Notes" in markdown


def test_registry_validation_does_not_write_temp_markdown(
portfolio_workspace: Path,
portfolio_catalog: Path,
legacy_registry: Path,
tmp_path: Path,
) -> None:
result = build_portfolio_truth_snapshot(
workspace_root=portfolio_workspace,
catalog_path=portfolio_catalog,
legacy_registry_path=legacy_registry,
include_notion=False,
)
markdown = render_registry_markdown(result.snapshot)
temp_path = tmp_path / "registry-check.md"

validate_registry_markdown(markdown, result.snapshot, temp_path)

assert not temp_path.exists()


def test_registry_render_surfaces_security_and_round_trips(
portfolio_workspace: Path,
portfolio_catalog: Path,
Expand Down
11 changes: 9 additions & 2 deletions tests/test_registry_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
from datetime import datetime, timezone

from github_repo_auditor.models import RepoAudit, RepoMetadata
from github_repo_auditor.registry_parser import _normalize, parse_registry, reconcile
from github_repo_auditor.registry_parser import (
_normalize,
parse_registry,
parse_registry_markdown,
reconcile,
)


def _make_audit(name: str, tier: str = "functional", score: float = 0.6) -> RepoAudit:
Expand Down Expand Up @@ -47,16 +52,18 @@ def test_strips_readiness_suffix(self):
class TestParseRegistry:
def test_parses_simple_table(self, tmp_path):
registry = tmp_path / "registry.md"
registry.write_text(
markdown = (
"# Projects\n\n"
"| Project | Status | Notes |\n"
"|---------|--------|-------|\n"
"| Alpha | active | Good |\n"
"| Beta | parked | Stale |\n"
"| Gamma | archived | Legacy |\n"
)
registry.write_text(markdown)
result = parse_registry(registry)
assert result == {"Alpha": "active", "Beta": "parked", "Gamma": "archived"}
assert parse_registry_markdown(markdown) == result

def test_skips_invalid_status(self, tmp_path):
registry = tmp_path / "registry.md"
Expand Down