|
| 1 | +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 2 | +# SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +name: "CI: Restricted Paths Guard" |
| 5 | + |
| 6 | +on: |
| 7 | + # Run on drafts too so maintainers get early awareness on WIP PRs. |
| 8 | + # Label updates on fork PRs require pull_request_target permissions. |
| 9 | + pull_request_target: |
| 10 | + types: |
| 11 | + - opened |
| 12 | + - synchronize |
| 13 | + - reopened |
| 14 | + - ready_for_review |
| 15 | + |
| 16 | +jobs: |
| 17 | + restricted-paths-guard: |
| 18 | + name: Apply review label if needed |
| 19 | + if: github.repository_owner == 'NVIDIA' |
| 20 | + runs-on: ubuntu-latest |
| 21 | + permissions: |
| 22 | + pull-requests: write |
| 23 | + steps: |
| 24 | + - name: Inspect PR author signals for restricted paths |
| 25 | + env: |
| 26 | + # PR metadata inputs |
| 27 | + AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association || 'NONE' }} |
| 28 | + PR_AUTHOR: ${{ github.event.pull_request.user.login }} |
| 29 | + PR_NUMBER: ${{ github.event.pull_request.number }} |
| 30 | + PR_URL: ${{ github.event.pull_request.html_url }} |
| 31 | + |
| 32 | + # Workflow policy inputs |
| 33 | + REVIEW_LABEL: Needs-Restricted-Paths-Review |
| 34 | + |
| 35 | + # API request context/auth |
| 36 | + GH_TOKEN: ${{ github.token }} |
| 37 | + REPO: ${{ github.repository }} |
| 38 | + run: | |
| 39 | + set -euo pipefail |
| 40 | +
|
| 41 | + if ! MATCHING_RESTRICTED_PATHS=$( |
| 42 | + gh api \ |
| 43 | + --paginate \ |
| 44 | + --jq ' |
| 45 | + .[] |
| 46 | + | select( |
| 47 | + (.filename | startswith("cuda_bindings/")) |
| 48 | + or ((.previous_filename // "") | startswith("cuda_bindings/")) |
| 49 | + or (.filename | startswith("cuda_python/")) |
| 50 | + or ((.previous_filename // "") | startswith("cuda_python/")) |
| 51 | + ) |
| 52 | + | if (.previous_filename // "") != "" then |
| 53 | + "\(.previous_filename) -> \(.filename)" |
| 54 | + else |
| 55 | + .filename |
| 56 | + end |
| 57 | + ' \ |
| 58 | + "repos/$REPO/pulls/$PR_NUMBER/files" |
| 59 | + ); then |
| 60 | + echo "::error::Failed to inspect the PR file list." |
| 61 | + { |
| 62 | + echo "## Restricted Paths Guard Failed" |
| 63 | + echo "" |
| 64 | + echo "- **Error**: Failed to inspect the PR file list." |
| 65 | + echo "- **Author**: $PR_AUTHOR" |
| 66 | + echo "- **Author association**: $AUTHOR_ASSOCIATION" |
| 67 | + echo "" |
| 68 | + echo "Please update the PR at: $PR_URL" |
| 69 | + } >> "$GITHUB_STEP_SUMMARY" |
| 70 | + exit 1 |
| 71 | + fi |
| 72 | +
|
| 73 | + # Fetch live PR labels to avoid stale event payload (race condition |
| 74 | + # when labels are changed shortly before the workflow runs). |
| 75 | + if ! LIVE_LABELS=$( |
| 76 | + gh pr view "${PR_NUMBER}" --repo "${REPO}" \ |
| 77 | + --json labels \ |
| 78 | + --jq '[.labels[].name]' |
| 79 | + ); then |
| 80 | + echo "::error::Failed to inspect the current PR labels." |
| 81 | + { |
| 82 | + echo "## Restricted Paths Guard Failed" |
| 83 | + echo "" |
| 84 | + echo "- **Error**: Failed to inspect the current PR labels." |
| 85 | + echo "- **Author**: $PR_AUTHOR" |
| 86 | + echo "- **Author association**: $AUTHOR_ASSOCIATION" |
| 87 | + echo "" |
| 88 | + echo "Please update the PR at: $PR_URL" |
| 89 | + } >> "$GITHUB_STEP_SUMMARY" |
| 90 | + exit 1 |
| 91 | + fi |
| 92 | +
|
| 93 | + TOUCHES_RESTRICTED_PATHS=false |
| 94 | + if [ -n "$MATCHING_RESTRICTED_PATHS" ]; then |
| 95 | + TOUCHES_RESTRICTED_PATHS=true |
| 96 | + fi |
| 97 | +
|
| 98 | + write_matching_restricted_paths() { |
| 99 | + echo "- **Matched restricted paths**:" |
| 100 | + echo '```text' |
| 101 | + printf '%s\n' "$MATCHING_RESTRICTED_PATHS" |
| 102 | + echo '```' |
| 103 | + } |
| 104 | +
|
| 105 | + HAS_TRUSTED_SIGNAL=false |
| 106 | + LABEL_ACTION="not needed (no restricted paths)" |
| 107 | + TRUSTED_SIGNALS="(none)" |
| 108 | +
|
| 109 | + if [ "$TOUCHES_RESTRICTED_PATHS" = "true" ]; then |
| 110 | + case "$AUTHOR_ASSOCIATION" in |
| 111 | + COLLABORATOR|MEMBER|OWNER) |
| 112 | + HAS_TRUSTED_SIGNAL=true |
| 113 | + LABEL_ACTION="not needed (author association is a trusted signal)" |
| 114 | + TRUSTED_SIGNALS="author_association:$AUTHOR_ASSOCIATION" |
| 115 | + ;; |
| 116 | + esac |
| 117 | + fi |
| 118 | +
|
| 119 | + NEEDS_REVIEW_LABEL=false |
| 120 | + if [ "$TOUCHES_RESTRICTED_PATHS" = "true" ] && [ "$HAS_TRUSTED_SIGNAL" = "false" ]; then |
| 121 | + NEEDS_REVIEW_LABEL=true |
| 122 | + fi |
| 123 | +
|
| 124 | + LABEL_ALREADY_PRESENT=false |
| 125 | + if jq -e --arg label "$REVIEW_LABEL" '.[] == $label' <<<"$LIVE_LABELS" >/dev/null; then |
| 126 | + LABEL_ALREADY_PRESENT=true |
| 127 | + fi |
| 128 | +
|
| 129 | + if [ "$NEEDS_REVIEW_LABEL" = "true" ]; then |
| 130 | + if [ "$LABEL_ALREADY_PRESENT" = "true" ]; then |
| 131 | + LABEL_ACTION="already present" |
| 132 | + elif ! gh pr edit "$PR_NUMBER" --repo "$REPO" --add-label "$REVIEW_LABEL"; then |
| 133 | + echo "::error::Failed to add the $REVIEW_LABEL label." |
| 134 | + { |
| 135 | + echo "## Restricted Paths Guard Failed" |
| 136 | + echo "" |
| 137 | + echo "- **Error**: Failed to add the \`$REVIEW_LABEL\` label." |
| 138 | + echo "- **Author**: $PR_AUTHOR" |
| 139 | + echo "- **Author association**: $AUTHOR_ASSOCIATION" |
| 140 | + echo "" |
| 141 | + write_matching_restricted_paths |
| 142 | + echo "" |
| 143 | + echo "Please update the PR at: $PR_URL" |
| 144 | + } >> "$GITHUB_STEP_SUMMARY" |
| 145 | + exit 1 |
| 146 | + else |
| 147 | + LABEL_ACTION="added" |
| 148 | + fi |
| 149 | + elif [ "$LABEL_ALREADY_PRESENT" = "true" ]; then |
| 150 | + LABEL_ACTION="left in place (manual removal required)" |
| 151 | + fi |
| 152 | +
|
| 153 | + { |
| 154 | + echo "## Restricted Paths Guard Completed" |
| 155 | + echo "" |
| 156 | + echo "- **Author**: $PR_AUTHOR" |
| 157 | + echo "- **Author association**: $AUTHOR_ASSOCIATION" |
| 158 | + echo "- **Touches restricted paths**: $TOUCHES_RESTRICTED_PATHS" |
| 159 | + echo "- **Restricted paths**: \`cuda_bindings/\`, \`cuda_python/\`" |
| 160 | + echo "- **Trusted signals**: $TRUSTED_SIGNALS" |
| 161 | + echo "- **Label action**: $LABEL_ACTION" |
| 162 | + if [ "$TOUCHES_RESTRICTED_PATHS" = "true" ]; then |
| 163 | + echo "" |
| 164 | + write_matching_restricted_paths |
| 165 | + fi |
| 166 | + if [ "$NEEDS_REVIEW_LABEL" = "true" ]; then |
| 167 | + echo "" |
| 168 | + echo "- **Manual follow-up**: No trusted signal was found, so \`$REVIEW_LABEL\` is required." |
| 169 | + elif [ "$LABEL_ALREADY_PRESENT" = "true" ]; then |
| 170 | + echo "" |
| 171 | + echo "- **Manual follow-up**: Existing \`$REVIEW_LABEL\` was left in place intentionally because this workflow does not inspect every commit. Remove it manually after reviewing the PR for restricted-paths policy compliance." |
| 172 | + fi |
| 173 | + } >> "$GITHUB_STEP_SUMMARY" |
0 commit comments