Skip to content
Open
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
4 changes: 4 additions & 0 deletions .agents/skills/watch-github-actions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ gh run list --json databaseId,status,headBranch,url --jq '.[] | {id: .databaseId

## View Job Logs

For Branch Checks `Reviewer Roster` failures, the log lists non-collaborators in
`REVIEWERS.md`. Remove those entries or arrange repository access before rerunning.
Lookup errors fail the check rather than treating missing results as eligibility.

For Kubernetes app-listener E2E failures during kind setup, inspect the uploaded
`results/app-listener/cni-setup/` evidence. It includes node and Calico pod
descriptions, events, and current/previous container logs. A CNI rollout timeout
Expand Down
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
/REVIEWERS.md @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr
/tasks/scripts/check_review_readiness.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr
/tasks/scripts/check_review_readiness_test.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr
/tasks/scripts/check_reviewer_collaborators.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr
/tasks/scripts/check_reviewer_collaborators_test.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr

# PR issue acceptance checks
/tasks/scripts/check_issue_acceptance.cjs @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/branch-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ jobs:
echo "number=0" >> "$GITHUB_OUTPUT"
fi

reviewer-roster:
name: Reviewer Roster
needs: pr_metadata
if: needs.pr_metadata.outputs.should_run == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Validate reviewer collaborator membership
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: python3 tasks/scripts/check_reviewer_collaborators.py --repo "$GH_REPO"

compatibility-review:
name: Compatibility Review (advisory)
needs: pr_metadata
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ Pull requests that touch the enforcement machinery or reviewer routing — anyth

Once your pull request represents [accepted work](#accepted-work), apply exactly one `area:` label and any relevant `topic:` labels. Follow [Labeling and Notifications](#labeling-and-notifications) before applying classification labels. The [reviewer routing workflow](.github/workflows/review-readiness.yml) requests all matching members of [REVIEWERS.md](REVIEWERS.md), excluding the author, when a PR opens ready for review, leaves draft status, or gains or loses an area/topic label. Reviewers do not need repository write access to receive requests or review PRs. Manual reviewer choices and people who have commented or reviewed are preserved. Propose roster membership changes through a reviewed PR.

Branch Checks rejects accounts in `REVIEWERS.md` that are not repository collaborators. Public read access alone does not qualify; read and triage collaborators are eligible.

If your pull request has been idle, ask for a reviewer in the pull request or in the CNCF Slack channel rather than waiting.

## Agent Skills
Expand Down
21 changes: 0 additions & 21 deletions REVIEWERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ Authentication, multitenancy, gateway interceptors, FIPS compliance, and other e

| Name | GitHub ID | Company/Organization |
| --- | --- | --- |
| Akram Ben Aissi | [@akram](https://github.com/akram) | Red Hat |
| Derek Carr | [@derekwaynecarr](https://github.com/derekwaynecarr) | Red Hat |
| Seth Jennings | [@sjenning](https://github.com/sjenning) | Red Hat |

Expand Down Expand Up @@ -88,7 +87,6 @@ Authentication, authorization, credential protection, policy integrity/signed po

| Name | GitHub ID | Company/Organization |
| --- | --- | --- |
| Gordon Sim | [@grs](https://github.com/grs) | Red Hat |
| Kris Hicks | [@krishicks](https://github.com/krishicks) | NVIDIA |
| Seth Jennings | [@sjenning](https://github.com/sjenning) | Red Hat |

Expand Down Expand Up @@ -129,7 +127,6 @@ Extension APIs and integration points, including consistency, general-purpose de

| Name | GitHub ID | Company/Organization |
| --- | --- | --- |
| Artem Lytvyn | [@letv1nnn](https://github.com/letv1nnn) | Independent |
| Drew Newberry | [@drew](https://github.com/drew) | NVIDIA |
| Matthew Grossman | [@matthewgrossman](https://github.com/matthewgrossman) | NVIDIA |
| Piotr Mlocek | [@pimlock](https://github.com/pimlock) | NVIDIA |
Expand All @@ -151,11 +148,7 @@ Kubernetes operator development, custom resource definitions (CRDs), deployment
| --- | --- | --- |
| Divesh Chowdary | [@FrostGod](https://github.com/FrostGod) | NVIDIA |
| Drew Newberry | [@drew](https://github.com/drew) | NVIDIA |
| Ignas Baranauskas | [@Ygnas](https://github.com/Ygnas) | Red Hat |
| Jorge Garcia Oncins | [@jgarciao](https://github.com/jgarciao) | Red Hat |
| Kris Hicks | [@krishicks](https://github.com/krishicks) | NVIDIA |
| Krzysztof Malczuk | [@2000krysztof](https://github.com/2000krysztof) | Red Hat |
| Mark Campbell | [@Bobbins228](https://github.com/Bobbins228) | Red Hat |
| Mrunal Patel | [@mrunalp](https://github.com/mrunalp) | Red Hat |
| Taylor Mutch | [@TaylorMutch](https://github.com/TaylorMutch) | NVIDIA |

Expand Down Expand Up @@ -186,11 +179,8 @@ Logging, metrics, tracing, telemetry, dashboards, event correlation, and the col

| Name | GitHub ID | Company/Organization |
| --- | --- | --- |
| Artem Lytvyn | [@letv1nnn](https://github.com/letv1nnn) | Independent |
| Gaizka Menendez | [@gmenher](https://github.com/gmenher) | Red Hat |
| Kris Hicks | [@krishicks](https://github.com/krishicks) | NVIDIA |
| Matthew Grossman | [@matthewgrossman](https://github.com/matthewgrossman) | NVIDIA |
| Roland Huss | [@rhuss](https://github.com/rhuss) | Red Hat |
| Taylor Mutch | [@TaylorMutch](https://github.com/TaylorMutch) | NVIDIA |

### topic:operability
Expand Down Expand Up @@ -242,7 +232,6 @@ Go SDK implementation, maintenance, interfaces, and distribution.

| Name | GitHub ID | Company/Organization |
| --- | --- | --- |
| Krzysztof Malczuk | [@2000krysztof](https://github.com/2000krysztof) | Red Hat |
| Max Dubrinsky | [@maxdubrinsky](https://github.com/maxdubrinsky) | Gretel |
| Simon Scatton | [@SDAChess](https://github.com/SDAChess) | NVIDIA |
| Taylor Mutch | [@TaylorMutch](https://github.com/TaylorMutch) | NVIDIA |
Expand All @@ -253,7 +242,6 @@ Python SDK implementation, maintenance, interfaces, and distribution.

| Name | GitHub ID | Company/Organization |
| --- | --- | --- |
| Krzysztof Malczuk | [@2000krysztof](https://github.com/2000krysztof) | Red Hat |
| Max Dubrinsky | [@maxdubrinsky](https://github.com/maxdubrinsky) | Gretel |
| Simon Scatton | [@SDAChess](https://github.com/SDAChess) | NVIDIA |
| Taylor Mutch | [@TaylorMutch](https://github.com/TaylorMutch) | NVIDIA |
Expand All @@ -264,7 +252,6 @@ Rust SDK implementation, maintenance, interfaces, and distribution.

| Name | GitHub ID | Company/Organization |
| --- | --- | --- |
| Krzysztof Malczuk | [@2000krysztof](https://github.com/2000krysztof) | Red Hat |
| Max Dubrinsky | [@maxdubrinsky](https://github.com/maxdubrinsky) | Gretel |
| Simon Scatton | [@SDAChess](https://github.com/SDAChess) | NVIDIA |
| Taylor Mutch | [@TaylorMutch](https://github.com/TaylorMutch) | NVIDIA |
Expand All @@ -275,7 +262,6 @@ TypeScript SDK implementation, maintenance, interfaces, and distribution.

| Name | GitHub ID | Company/Organization |
| --- | --- | --- |
| Krzysztof Malczuk | [@2000krysztof](https://github.com/2000krysztof) | Red Hat |
| Max Dubrinsky | [@maxdubrinsky](https://github.com/maxdubrinsky) | Gretel |
| Simon Scatton | [@SDAChess](https://github.com/SDAChess) | NVIDIA |
| Taylor Mutch | [@TaylorMutch](https://github.com/TaylorMutch) | NVIDIA |
Expand All @@ -287,10 +273,8 @@ Vulnerability management, static/dynamic analysis, FSR (Functional Security Requ
| Name | GitHub ID | Company/Organization |
| --- | --- | --- |
| Adrien Langou | [@alangou](https://github.com/alangou) | NVIDIA |
| Christian Zaccaria | [@ChristianZaccaria](https://github.com/ChristianZaccaria) | Red Hat |
| Jim Meyer | [@purp](https://github.com/purp) | NVIDIA |
| John T. Myers | [@johntmyers](https://github.com/johntmyers) | NVIDIA |
| Krzysztof Malczuk | [@2000krysztof](https://github.com/2000krysztof) | Red Hat |
| Russell Bryant | [@russellb](https://github.com/russellb) | Red Hat |
| Simon Scatton | [@SDAChess](https://github.com/SDAChess) | NVIDIA |

Expand All @@ -301,9 +285,6 @@ Unit, integration, regression, conformance, and end-to-end testing, including te
| Name | GitHub ID | Company/Organization |
| --- | --- | --- |
| Evan Lezar | [@elezar](https://github.com/elezar) | NVIDIA |
| Gaizka Menendez | [@gmenher](https://github.com/gmenher) | Red Hat |
| Jorge Garcia Oncins | [@jgarciao](https://github.com/jgarciao) | Red Hat |
| Mark Campbell | [@Bobbins228](https://github.com/Bobbins228) | Red Hat |
| Matthew Grossman | [@matthewgrossman](https://github.com/matthewgrossman) | NVIDIA |
| Simon Scatton | [@SDAChess](https://github.com/SDAChess) | NVIDIA |

Expand All @@ -313,5 +294,3 @@ CLI command interface and flags, help text, error messages and exit codes, human

| Name | GitHub ID | Company/Organization |
| --- | --- | --- |
| Artem Lytvyn | [@letv1nnn](https://github.com/letv1nnn) | Independent |
| Roland Huss | [@rhuss](https://github.com/rhuss) | Red Hat |
47 changes: 47 additions & 0 deletions tasks/scripts/check_reviewer_collaborators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Reject reviewer roster entries without repository collaborator membership."""

import argparse
import subprocess
import sys
from pathlib import Path

from check_review_readiness import _api, parse_reviewers


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo", required=True)
parser.add_argument("--reviewers", type=Path, default=Path("REVIEWERS.md"))
args = parser.parse_args(argv)
try:
categories = parse_reviewers(args.reviewers.read_text(encoding="utf-8"))
members = set().union(*categories.values())
collaborators = {
member["login"].lower()
for member in _api(
f"repos/{args.repo}/collaborators?per_page=100&affiliation=all",
pages=True,
)
}
invalid = sorted(members - collaborators)
if invalid:
raise ValueError("non-collaborators in REVIEWERS.md: " + ", ".join(invalid))
except subprocess.CalledProcessError as exc:
print(
"Cannot list repository collaborators: " + (exc.stderr or str(exc)).strip(),
file=sys.stderr,
)
return 1
except (OSError, ValueError, KeyError, TypeError) as exc:
print(f"Reviewer roster: {exc}", file=sys.stderr)
return 1
print(f"All {len(members)} reviewer roster accounts are repository collaborators.")
return 0


if __name__ == "__main__":
sys.exit(main())
82 changes: 82 additions & 0 deletions tasks/scripts/check_reviewer_collaborators_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Offline tests for reviewer roster eligibility."""

import contextlib
import io
import json
import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest import mock

import check_reviewer_collaborators as check

ROSTER = """### area:build

| Name | GitHub ID | Company/Organization |
| --- | --- | --- |
| Alice | [@Alice](https://github.com/Alice) | Example |
| Bob | [@bob](https://github.com/bob) | Example |
"""


class CollaboratorTests(unittest.TestCase):
def run_check(self, response):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "REVIEWERS.md"
path.write_text(ROSTER)
with (
mock.patch.object(check.subprocess, "run", side_effect=response) as api,
contextlib.redirect_stdout(io.StringIO()) as output,
contextlib.redirect_stderr(io.StringIO()) as errors,
):
result = check.main(["--repo", "owner/repo", "--reviewers", str(path)])
return result, api, output.getvalue(), errors.getvalue()

def test_accepts_collaborators_on_all_pages_without_write_access(self):
pages = [
[{"login": "ALICE", "role_name": "read"}],
[{"login": "bob", "role_name": "triage"}],
]
result, api, _, _ = self.run_check(
[subprocess.CompletedProcess([], 0, stdout=json.dumps(pages))]
)
self.assertEqual(result, 0)
command = api.call_args.args[0]
self.assertIn(
"repos/owner/repo/collaborators?per_page=100&affiliation=all", command
)
self.assertIn("--paginate", command)
self.assertIn("--slurp", command)

def test_reports_all_non_collaborators_in_sorted_order(self):
result, _, _, errors = self.run_check(
[subprocess.CompletedProcess([], 0, stdout="[[]]")]
)
self.assertEqual(result, 1)
self.assertIn("alice, bob", errors)
self.assertIn("REVIEWERS.md", errors)

def test_lookup_failure_fails_closed(self):
result, _, output, errors = self.run_check(
subprocess.CalledProcessError(1, "gh", stderr="Forbidden (HTTP 403)")
)
self.assertEqual(result, 1)
self.assertEqual(output, "")
self.assertIn("403", errors)

def test_invalid_roster_fails_before_lookup(self):
with mock.patch.object(
check, "parse_reviewers", side_effect=ValueError("bad roster")
):
result, api, _, errors = self.run_check([])
self.assertEqual(result, 1)
api.assert_not_called()
self.assertIn("bad roster", errors)


if __name__ == "__main__":
unittest.main()
5 changes: 4 additions & 1 deletion tasks/test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ hide = true

["test:review-readiness"]
description = "Test PR review readiness and reviewer reconciliation"
run = "uv run --no-project python -m unittest discover -s tasks/scripts -p check_review_readiness_test.py"
run = [
"uv run --no-project python -m unittest discover -s tasks/scripts -p check_review_readiness_test.py",
"uv run --no-project python -m unittest discover -s tasks/scripts -p check_reviewer_collaborators_test.py",
]
hide = true

["test:accepted-work"]
Expand Down
Loading