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
28 changes: 27 additions & 1 deletion doc_steward/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@
from .artefact_index_validator import check_artefact_index
from .dap_validator import check_dap_compliance
from .doc_authority_manifest import check_doc_authority_manifest
from .entrypoint_freshness_sweep import run as run_entrypoint_freshness_sweep
from .entrypoint_freshness_sweep import (
DEFAULT_LABELS as ENTRYPOINT_FRESHNESS_LABELS,
)
from .entrypoint_freshness_sweep import (
run as run_entrypoint_freshness_sweep,
)
from .entrypoint_freshness_sweep import (
validate_repo_labels as validate_entrypoint_freshness_labels,
)
from .freshness_validator import check_freshness, get_freshness_mode
from .global_archive_link_ban_validator import check_global_archive_link_ban
from .index_checker import check_index
Expand Down Expand Up @@ -161,6 +169,19 @@ def cmd_freshness_check(args: argparse.Namespace) -> int:

def cmd_entrypoint_freshness_sweep(args: argparse.Namespace) -> int:
"""Run entrypoint freshness issue-creator adapter."""
if args.validate_labels:
payload = validate_entrypoint_freshness_labels(args.repo, list(ENTRYPOINT_FRESHNESS_LABELS))
if args.json:
import json

print(json.dumps(payload, indent=2, sort_keys=True))
else:
print(
"[PASSED] entrypoint freshness labels valid"
if payload["valid"]
else "[FAILED] entrypoint freshness labels missing"
)
return 0 if payload["valid"] else 1
run_entrypoint_freshness_sweep(
Path(args.repo_root).resolve(),
repo=args.repo,
Expand Down Expand Up @@ -359,6 +380,11 @@ def main() -> int:
help="write a sweep_lib run receipt; rejected with --dry-run",
)
p_entrypoint_sweep.add_argument("--json", action="store_true")
p_entrypoint_sweep.add_argument(
"--validate-labels",
action="store_true",
help="read-only preflight that configured GitHub labels exist",
)
p_entrypoint_sweep.set_defaults(func=cmd_entrypoint_freshness_sweep)

# protocols-structure-check
Expand Down
58 changes: 58 additions & 0 deletions doc_steward/entrypoint_freshness_sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,41 @@ def _as_int(value: object) -> int:
return value if isinstance(value, int) else 0


def gh_list_labels(repo: str) -> set[str]:
"""Read live GitHub labels for explicit preflight validation."""
proc = subprocess.run(
["gh", "label", "list", "-R", repo, "--limit", "200", "--json", "name"],
text=True,
capture_output=True,
timeout=90,
)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip())
raw = json.loads(proc.stdout or "[]")
return {str(item.get("name")) for item in raw if item.get("name")}


def validate_repo_labels(
repo: str,
labels: list[str],
*,
existing_labels: set[str] | None = None,
) -> dict[str, object]:
"""Validate configured issue labels before an issue-creating run.

This is read-only. It prevents a late `gh issue create` failure and gives the
sweep a deterministic label preflight receipt.
"""
available = existing_labels if existing_labels is not None else gh_list_labels(repo)
missing = [label for label in labels if label not in available]
return {
"repo": repo,
"labels": labels,
"missing_labels": missing,
"valid": not missing,
}


def gh_create_issue(repo: str, title: str, body: str, labels: list[str]) -> int:
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as handle:
handle.write(body)
Expand Down Expand Up @@ -188,6 +223,12 @@ def process_findings(
updated = 0
if action == "created":
if create_issue:
label_result = validate_repo_labels(repo, labels)
if not label_result["valid"]:
raise RuntimeError(
"missing GitHub labels for doc-entrypoint freshness issue: "
+ ", ".join(str(label) for label in label_result["missing_labels"])
)
issue_num = gh_create_issue(repo, title, body, labels)
db.upsert_finding(
fingerprint,
Expand Down Expand Up @@ -282,9 +323,26 @@ def main(argv: list[str] | None = None) -> int:
help="write a sweep_lib run receipt; rejected with --dry-run",
)
parser.add_argument("--json", action="store_true")
parser.add_argument(
"--validate-labels",
action="store_true",
help="read-only preflight that configured GitHub labels exist",
)
args = parser.parse_args(argv)
if args.dry_run and args.record_run:
parser.error("--record-run mutates sweep receipts and cannot be combined with --dry-run")
if args.validate_labels:
payload = validate_repo_labels(args.repo, list(DEFAULT_LABELS))
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
else:
message = (
"[PASSED] entrypoint freshness labels valid"
if payload["valid"]
else "[FAILED] entrypoint freshness labels missing"
)
print(message)
return 0 if payload["valid"] else 1
run(
args.repo_root,
repo=args.repo,
Expand Down
64 changes: 64 additions & 0 deletions tests_doc/test_entrypoint_freshness_sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
normalize_error,
process_findings,
run,
validate_repo_labels,
)


Expand Down Expand Up @@ -97,6 +98,30 @@ def test_process_findings_created_records_one_finding_without_issue_creation():
assert db.closed is True


def test_process_findings_create_issue_validates_labels_before_issue_creation(monkeypatch):
db = FakeDB(action="created")
import doc_steward.entrypoint_freshness_sweep as sweep

monkeypatch.setattr(
sweep,
"validate_repo_labels",
lambda repo, labels: {
"repo": repo,
"labels": labels,
"missing_labels": ["severity:warning"],
"valid": False,
},
)

import pytest

with pytest.raises(RuntimeError, match="missing GitHub labels"):
process_findings([finding()], dry_run=False, create_issue=True, sweep_lib=fake_lib(db))

assert len(db.calls) == 1
assert db.calls[0][0][0].startswith("fp:inventory-hygiene-sweep:lifeos-doc-entrypoint")


def test_process_findings_updated_skips_duplicate_creation():
db = FakeDB(action="updated")

Expand All @@ -109,6 +134,32 @@ def test_process_findings_updated_skips_duplicate_creation():
assert db.closed is True


def test_validate_repo_labels_reports_missing_without_mutation():
result = validate_repo_labels(
"marcusglee11/lifeos-operational-bus",
["sweep:inventory-hygiene", "severity:warning"],
existing_labels={"sweep:inventory-hygiene"},
)

assert result == {
"repo": "marcusglee11/lifeos-operational-bus",
"labels": ["sweep:inventory-hygiene", "severity:warning"],
"missing_labels": ["severity:warning"],
"valid": False,
}


def test_validate_repo_labels_accepts_configured_labels():
result = validate_repo_labels(
"marcusglee11/lifeos-operational-bus",
["sweep:inventory-hygiene", "severity:warning"],
existing_labels={"sweep:inventory-hygiene", "severity:warning"},
)

assert result["valid"] is True
assert result["missing_labels"] == []


def test_process_findings_clean_state_has_no_rows():
result = process_findings([], dry_run=False, sweep_lib=fake_lib(FakeDB()))

Expand Down Expand Up @@ -142,6 +193,19 @@ def test_run_dirty_dry_run_does_not_load_sweep_lib(monkeypatch, tmp_path):
assert rows[0]["action"] == "dry-run"


def test_run_clean_dry_run_is_quiet_and_does_not_record_receipt(monkeypatch, tmp_path, capsys):
import doc_steward.entrypoint_freshness_sweep as sweep

monkeypatch.setattr(sweep, "check_entrypoint_freshness", lambda repo_root: [])
result = run(tmp_path, dry_run=True, json_output=False, record_run=False)
captured = capsys.readouterr()

assert captured.out.strip() == "[SILENT]"
assert result["findings"] == 0
assert result["rows"] == []
assert result["receipt"] is None


def test_cli_rejects_dry_run_record_run(tmp_path):
import pytest

Expand Down
Loading