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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/.gitattributes text eol=lf
.github/workflows/*.yml text eol=lf
*.md text eol=lf
scripts/validate-reviewer-routes.py text eol=lf
2 changes: 2 additions & 0 deletions .github/workflows/reviewer-route-contract-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
- "docs/**"
- "projects/**"
- "scripts/validate-reviewer-routes.py"
- "tools/sbom-diff-and-risk/*.md"
- "tools/sbom-diff-and-risk/README.md"
- "tools/sbom-diff-and-risk/docs/**"
- "tools/sbom-diff-and-risk/examples/**"
Expand All @@ -20,6 +21,7 @@ on:
- "docs/**"
- "projects/**"
- "scripts/validate-reviewer-routes.py"
- "tools/sbom-diff-and-risk/*.md"
- "tools/sbom-diff-and-risk/README.md"
- "tools/sbom-diff-and-risk/docs/**"
- "tools/sbom-diff-and-risk/examples/**"
Expand Down
72 changes: 71 additions & 1 deletion scripts/validate-reviewer-routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@
Path("projects"),
)

WORKFLOW_PATH = Path(".github/workflows/reviewer-route-contract-ci.yml")
WORKFLOW_EVENTS_WITH_PATH_FILTERS = ("push", "pull_request")
REQUIRED_WORKFLOW_PATH_FILTERS = (
".github/workflows/reviewer-route-contract-ci.yml",
"README.md",
"docs/**",
"projects/**",
"scripts/validate-reviewer-routes.py",
"tools/sbom-diff-and-risk/*.md",
"tools/sbom-diff-and-risk/docs/**",
"tools/sbom-diff-and-risk/examples/**",
)

REQUIRED_LINK_TARGETS = {
Path("README.md"): {
"docs/reviewer-brief.md",
Expand Down Expand Up @@ -117,6 +130,7 @@
"Artifact evidence map",
"Reviewer route contract",
"Markdown links across the reviewer surface resolve",
"workflow path filters cover reviewer-surface changes",
"python scripts/validate-reviewer-routes.py",
"No network",
"not current PyPI package truth",
Expand Down Expand Up @@ -284,6 +298,46 @@ def iter_reviewer_surface_markdown(errors: list[str]) -> tuple[Path, ...]:
return tuple(markdown_paths)


def workflow_path_filters(workflow_path: Path, event_name: str, errors: list[str]) -> set[str]:
absolute_path = REPO_ROOT / workflow_path
if not absolute_path.is_file():
errors.append(f"missing reviewer route workflow: {workflow_path.as_posix()}")
return set()

filters: set[str] = set()
in_event = False
in_paths = False

for line in absolute_path.read_text(encoding="utf-8").splitlines():
if not in_event:
if line == f" {event_name}:":
in_event = True
continue

if line and not line.startswith(" "):
break

if line.startswith(" ") and not line.startswith(" "):
break

if line.strip() == "paths:":
in_paths = True
continue

if not in_paths:
continue

if not line.startswith(" - "):
if line.strip():
in_paths = False
continue

raw_filter = line.split("- ", 1)[1].strip()
filters.add(raw_filter.strip("\"'"))

return filters


def iter_local_links(markdown_path: Path) -> set[str]:
text = read_markdown(markdown_path)
raw_targets = INLINE_LINK_RE.findall(text)
Expand Down Expand Up @@ -368,6 +422,21 @@ def validate_required_paths(errors: list[str]) -> None:
errors.append(f"missing supporting-project boundary file: {path.as_posix()}")


def validate_workflow_path_filters(errors: list[str]) -> None:
for event_name in WORKFLOW_EVENTS_WITH_PATH_FILTERS:
filters = workflow_path_filters(WORKFLOW_PATH, event_name, errors)
if not filters:
errors.append(f"{WORKFLOW_PATH}: missing path filters for {event_name}")
continue

for required_filter in REQUIRED_WORKFLOW_PATH_FILTERS:
if required_filter not in filters:
errors.append(
f"{WORKFLOW_PATH}: {event_name} is missing path filter "
f"{required_filter!r}"
)


def main() -> int:
errors: list[str] = []
reviewer_surface_markdown = iter_reviewer_surface_markdown(errors)
Expand All @@ -384,6 +453,7 @@ def main() -> int:
validate_required_text(markdown_path, errors)

validate_required_paths(errors)
validate_workflow_path_filters(errors)

if errors:
print("Reviewer route validation failed:", file=sys.stderr)
Expand All @@ -396,7 +466,7 @@ def main() -> int:
f"{len(DOCS_TO_VALIDATE)} documents and "
f"{len(REQUIRED_REVIEWER_PATHS)} reviewer paths checked; "
f"{len(reviewer_surface_markdown)} reviewer-surface markdown files "
"link-checked."
"link-checked; workflow path filters checked."
)
return 0

Expand Down
4 changes: 3 additions & 1 deletion tools/sbom-diff-and-risk/docs/reviewer-path.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ python scripts/validate-reviewer-routes.py
This checks that the repository reviewer route still has the expected local
links, markdown anchors, reviewer-path documents, supporting-project boundary
files, and required non-claim phrases. It also checks that Markdown links
across the reviewer surface resolve.
across the reviewer surface resolve and that workflow path filters cover
reviewer-surface changes.

Use this when you change reviewer-facing docs, examples, or supporting project
entry points. The contract lives in
Expand All @@ -147,6 +148,7 @@ Expected result:
- the SBOM reviewer path still links to the required evidence surfaces
- local markdown anchors resolve
- Markdown links across the reviewer surface resolve
- workflow path filters cover reviewer-surface changes
- supporting project reviewer paths and boundary files still exist
- required non-claims remain present in reviewer-facing docs

Expand Down