Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .agents/skills/create-github-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ GitHub built-in issue types (`Bug`, `Feature`, `Task`) should come from the matc

Creating an issue does not accept it. Follow Label Discovery in `CONTRIBUTING.md` to retrieve every page of current `state:*` label definitions and resolve unclear meanings, then follow its triage → validation → human acceptance process. Agents may assess facts, but only humans decide whether to accept work or place it on the roadmap. A direct user request authorizes the requested planning or implementation phase without changing issue disposition.

Follow [Labeling and Notifications](../../../CONTRIBUTING.md#labeling-and-notifications)
for classification labels. Defer area/topic labels until the work is accepted;
filing an issue or identifying its subject is not acceptance. Intake workflow
labels may still record triage status. Classification can trigger subscribers'
GitHub app in Slack and other integration notifications.

## Useful Options

| Option | Description |
Expand Down
5 changes: 5 additions & 0 deletions .agents/skills/create-github-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ automatically on issue-label events.
Created PR [#123](https://github.com/OWNER/REPO/pull/123)
```

Follow the shared [accepted-work definition](../../../CONTRIBUTING.md#accepted-work)
and [Labeling and Notifications](../../../CONTRIBUTING.md#labeling-and-notifications)
when applying classification labels. Review-readiness applies the acceptance label
to qualifying PRs when they open; this does not replace existing PR requirements.

### Monitor Workflow Run (Optional)

If the user asks to wait for a green CI before posting the RFR, use this snippet to monitor the workflow run:
Expand Down
5 changes: 5 additions & 0 deletions .agents/skills/triage-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ For a specific issue, assess it when requested. For batch triage, first show the

Do not apply an acceptance state, place issues on the roadmap, or make a product investment decision. Do not introduce `agent:*` workflow labels. Follow `SECURITY.md` if the report may disclose a vulnerability; do not copy sensitive details into a public comment.

Follow [Labeling and Notifications](../../../CONTRIBUTING.md#labeling-and-notifications):
defer area/topic classification labels until the work is accepted. They drive
subscriber notifications, so technical classification alone must not send
unaccepted intake to those streams. Continue to record appropriate triage state.

## Assess one issue

1. Run `gh issue view <id> --json number,title,body,state,labels,author,comments`. Follow Label Discovery in `CONTRIBUTING.md` to retrieve every page of current `state:*` label definitions and resolve unclear meanings. Check for previous triage, new human evidence, an existing owner, and duplicate issues.
Expand Down
4 changes: 4 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,9 @@
/tasks/scripts/check_issue_acceptance.cjs @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr
/tasks/scripts/check_issue_acceptance.test.cjs @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr

# Accepted-work classification
/tasks/scripts/accepted_work.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr
/tasks/scripts/accepted_work_test.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr

# Auditable SBOM check
/tasks/scripts/verify-image-sbom.sh @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr
3 changes: 3 additions & 0 deletions .github/workflows/branch-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,9 @@ jobs:

- name: Test release qualification summary
run: mise run test:qualification-summary

- name: Test accepted-work library and review-readiness
run: mise run test:accepted-work test:review-readiness
sdk-go-freeze:
# The Go SDK moved to ai-openshell/sdk-go (#5031). Its directory stays only
# until the final deprecated sdk/go tag exists, so PRs must not add or change
Expand Down
28 changes: 19 additions & 9 deletions .github/workflows/review-readiness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:

permissions:
contents: read
issues: read
pull-requests: write

concurrency:
Expand All @@ -19,30 +20,39 @@ jobs:
reviewers:
name: OpenShell / Review Readiness
if: >-
!github.event.pull_request.draft &&
(github.event.action == 'opened' ||
github.event.action == 'ready_for_review' ||
startsWith(github.event.label.name, 'area:') ||
startsWith(github.event.label.name, 'topic:'))
github.event.action == 'opened' ||
(!github.event.pull_request.draft &&
(github.event.action == 'ready_for_review' ||
startsWith(github.event.label.name, 'area:') ||
startsWith(github.event.label.name, 'topic:')))
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# Use the merged roster and helper, never code from the PR head.
- name: Check out the reviewer roster and helper
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
ref: ${{ github.event.repository.default_branch }}
sparse-checkout: |
REVIEWERS.md
MAINTAINERS.md
tasks/scripts/check_review_readiness.py
tasks/scripts/accepted_work.py
tasks/scripts/check_maintainer_approval.py
sparse-checkout-cone-mode: false
persist-credentials: false

- name: Reconcile reviewer requests
- name: Label newly opened accepted work and reconcile reviewers
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_ACTION: ${{ github.event.action }}
shell: bash
run: |
python3 tasks/scripts/check_review_readiness.py \
--repo "$GH_REPO" --pr "$PR_NUMBER" --apply
set -euo pipefail
args=(--repo "$GH_REPO" --pr "$PR_NUMBER" --apply)
if [ "$PR_ACTION" = opened ]; then
args+=(--label-accepted-work)
fi
python3 tasks/scripts/check_review_readiness.py "${args[@]}"
1 change: 1 addition & 0 deletions .github/zizmor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ rules:
- pr-issue-acceptance.yml:6
- release-canary.yml:3
- required-ci-gates.yml:3
- review-readiness.yml:6
- vouch-check.yml:3
1 change: 1 addition & 0 deletions CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,7 @@ The bot's full administrator documentation is internal to NVIDIA. The only comma
| `.github/actions/pr-merge-base/action.yml` | Composite action that resolves and fetches the merge-base commit for `pull-request/<N>` push workflows. |
| `.github/workflows/required-ci-gates.yml` | Posts required PR-head and merge-group statuses for gated CI workflows. This is what branch protection and merge queue should require. |
| `.github/workflows/e2e-label-help.yml` | When a `test:e2e*` label is applied, posts a PR comment telling the maintainer the next manual step (re-run an existing workflow run, or `/ok to test <SHA>` to refresh the mirror). |
| `.github/workflows/review-readiness.yml` | Labels newly opened accepted-work PRs using the shared read-only acceptance library, including drafts; routes matching reviewers when a PR is ready and classified. |
| `.github/workflows/workflow-security.yml` | Runs informational Actionlint and High-severity Zizmor reports on GitHub-hosted runners. |
| `.github/workflows/dependency-review.yml` | Reports dependency changes when GitHub Dependency Graph is available; otherwise publishes a neutral warning. |
| `.github/workflows/codeql.yml` | Runs nightly informational CodeQL analysis on `main` for Rust and the Go, Python, and TypeScript SDKs and retains SARIF artifacts. |
Expand Down
31 changes: 30 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Every pull request must be approved by someone listed in [MAINTAINERS.md](MAINTA

Pull requests that touch the enforcement machinery or reviewer routing — anything under `.github/workflows/`, `.github/actions/`, `MAINTAINERS.md`, `REVIEWERS.md`, `.github/CODEOWNERS`, or the enforcement and reviewer-routing scripts in `tasks/scripts/` — additionally require a code owner's approval, listed in [.github/CODEOWNERS](.github/CODEOWNERS). The status check reads its inputs from `main`, but a workflow triggered by a review runs the pull request's own copy of the workflow file, so GitHub's native code owner requirement is what keeps a change from disabling the gate that would have blocked it.

Use exactly one `area:` label and any relevant `topic:` labels on your pull request. 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.
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.

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

Expand Down Expand Up @@ -131,6 +131,29 @@ A direct request to an agent authorizes the requested planning or implementation

Do not file suspected vulnerabilities as public issues. Follow [SECURITY.md](SECURITY.md). Use the specialized security skills for authorized review or remediation.

### Accepted Work

Accepted work is work the project intends to pursue. Work submitted by a maintainer listed in the merged [MAINTAINERS.md](MAINTAINERS.md), or someone with repository `WRITE`, `MAINTAIN`, or `ADMIN` access, qualifies immediately. Other issues require a maintainer's acceptance, recorded with `state:accepted`.

A PR represents accepted work when either:

- Its author is a maintainer or has repository write access as described above.
- It closes at least one issue in this repository that has `state:accepted`, and the PR author is that issue's submitter or a current assignee.

Use GitHub closing references such as `Closes #123` or `Fixes #123`. Merely mentioning an accepted issue does not qualify. Acceptance and the author relationship must hold on the same issue. The PR author determines eligibility, not the person triggering a workflow. Other tools and contributor skills use this shared definition; the importable `tasks/scripts/accepted_work.py` library implements the PR check.

Acceptance classifies work, not implementation quality or approval to merge. Existing issue-linking rules, the separate `PR Issue Acceptance` check (which requires every linked issue to be accepted for community authors), and maintainer/code-owner approvals still apply. Acceptance does not certify that the diff implements the issue, and the existing Dependabot gate exemption does not automatically qualify a PR as accepted work.

### Labeling and Notifications

Labels drive notification streams as well as classification. Contributors use the GitHub app in Slack, and other integrations, to follow issues and PRs filtered by area or topic. Applying those labels can notify subscribers immediately, before they have chosen to follow the individual item.

Apply `area:` and `topic:` classification labels only after the issue or PR meets the shared [accepted-work definition](#accepted-work). Once accepted, apply the relevant classification labels so subscribers can find the work.

Do not classify an issue merely because it was filed or its subject is clear. Waiting until acceptance keeps label-filtered streams focused on work the project intends to pursue and avoids sending unaccepted intake to subscribers. Workflow labels such as `state:triage-needed` still record intake status; they do not establish acceptance.

Automatic classification at issue creation and filters that exclude unaccepted work are being discussed separately in [#5261](https://github.com/NVIDIA/OpenShell/issues/5261). Until that policy changes, defer classification labels until acceptance.

### Label Discovery

Retrieve every page of repository labels and their descriptions:
Expand Down Expand Up @@ -459,6 +482,12 @@ Automated dependency update PRs, including Dependabot PRs, are exempt from the s

Authors with `WRITE`, `MAINTAIN`, or `ADMIN` access may open a PR that only fixes a broken build or flaky tests without a separate issue. Describe the failure in the PR instead. Authors without write access follow the normal issue and acceptance requirements.

### Automatic Acceptance Labeling

The existing [review-readiness workflow](.github/workflows/review-readiness.yml) checks [accepted work](#accepted-work) when a PR is opened and adds `state:accepted` if it qualifies. This includes drafts and PRs awaiting area/topic classification. Reviewer requests still wait until a PR is ready and has an area label.

This is an additive check at creation, not continuous reconciliation: later edits, permission changes, and issue-label or assignment changes do not refresh or remove the PR label. The shared library is read-only; review-readiness applies the label. The label does not replace the issue-acceptance gate or merge approvals.

### Branch Names

Use `<type>/<issue-id>-<short-description>/<github-username>` for contributor branches. Choose the Conventional Commit type that describes the work, such as `feat`, `fix`, `refactor`, `chore`, `perf`, `docs`, `test`, or `ci`. The description is a short lowercase slug separated by hyphens; the final component is your GitHub login, not initials.
Expand Down
148 changes: 148 additions & 0 deletions tasks/scripts/accepted_work.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Shared accepted-work classification; see CONTRIBUTING.md#accepted-work.

Use is_accepted_work with supplied facts, or check_pr_accepted_work with a
GitHub API callable. The latter accepts the same path/method/data/pages arguments
as check_review_readiness._api. Both return a bool; API errors propagate.
This library has no CLI, label writes, or workflow lifecycle of its own.
"""

from __future__ import annotations

import subprocess
from dataclasses import dataclass
from typing import TYPE_CHECKING
from urllib.parse import quote

if TYPE_CHECKING:
from collections.abc import Callable, Iterable, Iterator

ACCEPTED_LABEL = "state:accepted"
WRITE_PERMISSIONS = frozenset({"write", "maintain", "admin"})
CLOSING_ISSUES_QUERY = """
query($owner: String!, $repo: String!, $number: Int!, $cursor: String) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
closingIssuesReferences(first: 100, after: $cursor) {
nodes { number repository { nameWithOwner } }
pageInfo { hasNextPage endCursor }
}
}
}
}
"""


@dataclass(frozen=True)
class ClosingIssue:
"""Facts about an issue GitHub reports that the PR closes."""

repository: str
accepted: bool
author: str | None
assignees: frozenset[str] = frozenset()


def is_accepted_work(
author: str,
*,
repository: str,
maintainers: set[str],
permission: str,
closing_issues: Iterable[ClosingIssue] = (),
) -> bool:
"""Classify supplied facts without IO; the triggering actor is irrelevant."""
author = author.lower()
if author in {login.lower() for login in maintainers}:
return True
if permission in WRITE_PERMISSIONS:
return True
for issue in closing_issues:
if issue.repository.lower() != repository.lower() or not issue.accepted:
continue
participants = {login.lower() for login in issue.assignees}
if issue.author:
participants.add(issue.author.lower())
if author in participants:
return True
return False


def _closing_issue_facts(
repository: str, number: int, api: Callable
) -> Iterator[ClosingIssue]:
owner, repo = repository.split("/")
cursor = None
while True:
result = api(
"graphql",
method="POST",
data={
"query": CLOSING_ISSUES_QUERY,
"variables": {
"owner": owner,
"repo": repo,
"number": number,
"cursor": cursor,
},
},
)
if result.get("errors"):
raise ValueError("GitHub could not resolve closing issues")
pull = result["data"]["repository"]["pullRequest"]
connection = pull["closingIssuesReferences"]
for reference in connection["nodes"]:
source = reference["repository"]["nameWithOwner"]
if source.lower() != repository.lower():
continue
root = f"repos/{repository}/issues/{reference['number']}"
issue = api(root)
if "pull_request" in issue:
continue
labels = api(f"{root}/labels?per_page=100", pages=True)
yield ClosingIssue(
repository=source,
accepted=any(label["name"] == ACCEPTED_LABEL for label in labels),
author=issue["user"]["login"] if issue["user"] else None,
assignees=frozenset(user["login"] for user in issue["assignees"]),
)
if not connection["pageInfo"]["hasNextPage"]:
return
next_cursor = connection["pageInfo"]["endCursor"]
if not next_cursor or next_cursor == cursor:
raise ValueError("GitHub returned an invalid closing-issue cursor")
cursor = next_cursor


def check_pr_accepted_work(
repository: str, pr: dict, maintainers: set[str], *, api: Callable
) -> bool:
"""Load current facts using a read-only API callable and classify this PR.

Callers supply maintainer membership from a trusted merged roster. A 404
collaborator lookup means no repository access; other lookup errors propagate.
Maintainer and write-access authors need no closing-issue lookup.
"""
if not maintainers:
raise ValueError("Could not parse any maintainers from MAINTAINERS.md")
author = pr["user"]["login"]
permission = "none"
if author.lower() not in {login.lower() for login in maintainers}:
try:
result = api(
f"repos/{repository}/collaborators/"
f"{quote(author, safe='')}/permission"
)
permission = result["permission"]
except subprocess.CalledProcessError as exc:
if "(HTTP 404)" not in (exc.stderr or ""):
raise
return is_accepted_work(
author,
repository=repository,
maintainers=maintainers,
permission=permission,
closing_issues=_closing_issue_facts(repository, pr["number"], api),
)
Loading
Loading