Skip to content
Closed
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
102 changes: 102 additions & 0 deletions .github/workflows/maintainer-approval-status.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

name: Maintainer Approval Status

# workflow_run always runs the default branch's copy of this file with a
# write-capable token, fork pull requests included. The pull request cannot
# alter the decision logic, and a commit status failure sends no email.

on:
workflow_run:
workflows: [Maintainer Approval]
types: [completed]
Comment on lines +10 to +13

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

concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: false

jobs:
publish:
name: Publish maintainer approval status
if: >-
github.repository_owner == 'NVIDIA' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# Check out the default branch, never the pull request head. Both the
# maintainer list and the decision helper must come from main.
- name: Check out the maintainer list and helper
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
sparse-checkout: |
MAINTAINERS.md
tasks/scripts/check_maintainer_approval.py
sparse-checkout-cone-mode: false
persist-credentials: false

- name: Require an approving review from a maintainer
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
TRIGGER_EVENT: ${{ github.event.workflow_run.event }}
TRIGGER_TITLE: ${{ github.event.workflow_run.display_title }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
shell: bash
run: |
set -euo pipefail

# This context is the required status check. Changing it silently
# breaks the ruleset entry that gates merges on it.
post_status() {
gh api --method POST "repos/$GH_REPO/statuses/$HEAD_SHA" \
-f state="$1" \
-f context="OpenShell / Maintainer Approval" \
-f description="$2" \
-f target_url="$RUN_URL" >/dev/null
}

# Only GitHub can raise merge_group events, and a pull request
# enters the queue only after this status passed on its head.
if [ "$TRIGGER_EVENT" = "merge_group" ]; then
post_status success "Checked before entering the merge queue"
exit 0
fi

# The run name comes from the pull request's copy of the trigger
# workflow, so it is untrusted. Bind it to the run's commit below.
if [[ ! "$TRIGGER_TITLE" =~ ^Maintainer\ Approval\ for\ PR\ \#([0-9]+)$ ]]; then
echo "::warning::Unrecognized trigger run name; leaving the status unchanged"
exit 0
fi
PR_NUMBER="${BASH_REMATCH[1]}"

gh api "repos/$GH_REPO/pulls/$PR_NUMBER" > pr.json
if [ "$(jq -r '.state' pr.json)" != "open" ] \
|| [ "$(jq -r '.head.sha' pr.json)" != "$HEAD_SHA" ]; then
# Closed, or the head moved on. A new head has no status yet, so
# the required context stays pending until it is evaluated.
echo "PR #$PR_NUMBER is closed or no longer at $HEAD_SHA; nothing to do"
exit 0
fi

gh api --paginate "repos/$GH_REPO/pulls/$PR_NUMBER/reviews" --jq '.[]' \
| jq -s '.' > reviews.json

# Exits non-zero when no maintainer's latest decisive review is an
# approval, and when MAINTAINERS.md yields no handles.
if python3 tasks/scripts/check_maintainer_approval.py \
--maintainers MAINTAINERS.md \
--reviews reviews.json; then
post_status success "Approved by a maintainer"
else
post_status failure "Needs approval from a maintainer in MAINTAINERS.md"
fi
64 changes: 21 additions & 43 deletions .github/workflows/maintainer-approval.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,62 +3,40 @@

name: Maintainer Approval

# This workflow only signals that the gate needs re-evaluating. The decision
# runs in maintainer-approval-status.yml, which posts the required
# `OpenShell / Maintainer Approval` commit status. Keeping the decision out of
# this job's result means a missing approval never fails a run here, so
# reviewers are not emailed a failure for every comment.
#
# maintainer-approval-status.yml parses the PR number from this run name.
run-name: >-
${{ github.event.pull_request.number
&& format('Maintainer Approval for PR #{0}', github.event.pull_request.number)
|| 'Maintainer Approval for merge queue' }}

on:
merge_group:
types: [checks_requested]
pull_request_review:
types: [submitted, dismissed]

permissions:
contents: read
pull-requests: read
permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
maintainer-approval:
# This name is the required status check context. Changing it silently
# breaks the ruleset entry that gates merges on this job.
name: OpenShell / Maintainer Approval
# Merge-group checks do not need to repeat the PR approval evaluation.
trigger:
# Not the required context. Skipping or passing this job never satisfies
# the gate; only the commit status does.
name: Maintainer Approval Trigger
# Comment reviews, including thread replies, cannot change the outcome.
if: >-
github.repository_owner == 'NVIDIA' &&
github.event_name != 'merge_group' &&
github.event.pull_request.state == 'open'
github.event.review.state != 'commented'
runs-on: ubuntu-latest
timeout-minutes: 5
timeout-minutes: 1
steps:
# Check out the default branch, never the pull request head. Both the
# maintainer list and the decision helper must come from main: reading
# either from the pull request ref would let a contributor add themselves
# to the list, or rewrite the decision logic, and self-approve.
- name: Check out the maintainer list and helper
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
sparse-checkout: |
MAINTAINERS.md
tasks/scripts/check_maintainer_approval.py
sparse-checkout-cone-mode: false
persist-credentials: false

- name: Require an approving review from a maintainer
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
shell: bash
run: |
set -euo pipefail

gh api --paginate "repos/$GH_REPO/pulls/$PR_NUMBER/reviews" --jq '.[]' \
| jq -s '.' > reviews.json

# Exits non-zero when no maintainer's latest decisive review is an
# approval, and when MAINTAINERS.md yields no handles. That exit code
# is the check result; nothing is posted anywhere.
python3 tasks/scripts/check_maintainer_approval.py \
--maintainers MAINTAINERS.md \
--reviews reviews.json
- run: echo "Requesting maintainer approval evaluation"
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ Do not start substantial issue-backed work until a maintainer has accepted the i

Use agents and the repository skills as needed to understand the affected code, evaluate tradeoffs, implement the smallest coherent change, and verify it. The pull request should explain what changed and how it was tested; it should not substitute an agent transcript for the contributor's understanding.

Every pull request must be approved by someone listed in [MAINTAINERS.md](MAINTAINERS.md) before it can merge. This is enforced by the `OpenShell / Maintainer Approval` status check, which turns green once one of those reviewers approves. Reviews from other contributors are welcome and count toward the general approval requirement, but they do not satisfy this check.
Every pull request must be approved by someone listed in [MAINTAINERS.md](MAINTAINERS.md) before it can merge. This is enforced by the `OpenShell / Maintainer Approval` commit status, which turns green once one of those reviewers approves. That commit status, not the `Maintainer Approval Trigger` job, is the required status check context in the branch ruleset. A new push leaves it pending until a maintainer approves the new head. Reviews from other contributors are welcome and count toward the general approval requirement, but they do not satisfy this check.

Pull requests that touch the enforcement machinery itself — anything under `.github/workflows/`, `.github/actions/`, `MAINTAINERS.md`, `.github/CODEOWNERS`, or the enforcement 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.
Pull requests that touch the enforcement machinery itself — anything under `.github/workflows/`, `.github/actions/`, `MAINTAINERS.md`, `.github/CODEOWNERS`, or the enforcement scripts in `tasks/scripts/` — additionally require a code owner's approval, listed in [.github/CODEOWNERS](.github/CODEOWNERS). The status is decided by `main`'s copy of the workflow, but the review-triggered workflow that requests it runs the pull request's own copy, so GitHub's native code owner requirement is what keeps a change from disabling the gate that would have blocked it.

Maintainers are not requested automatically. 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
Loading