diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4665a9682a3..1fa777b22e9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -40,6 +40,20 @@ Please explain the issue and the solution in short. Please list clearly what are the relevant test(s) that can safeguard the changes in the PR. This helps us to ensure we have sufficient test coverage for the PR. --> +## PR Checklist + +Please review the following before submitting your PR: +- PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense. +- PR Follows [TRT-LLM CODING GUIDELINES](https://github.com/NVIDIA/TensorRT-LLM/blob/main/CODING_GUIDELINES.md) to the best of your knowledge. +- Test cases are provided for new code paths (see [test instructions](https://github.com/NVIDIA/TensorRT-LLM/tree/main/tests#1-how-does-the-ci-work)) +- Any new dependencies have been scanned for license and vulnerabilities +- [CODEOWNERS](https://github.com/NVIDIA/TensorRT-LLM/blob/main/.github/CODEOWNERS) updated if ownership changes +- Documentation updated as needed +- The reviewers assigned automatically/manually are appropriate for the PR. + + +- [ ] Please check this after reviewing the above items as appropriate for this PR. + ## GitHub Bot Help `/bot [-h] ['run', 'kill', 'skip', 'reuse-pipeline'] ...` diff --git a/.github/scripts/pr_checklist_check.py b/.github/scripts/pr_checklist_check.py new file mode 100644 index 00000000000..e0c40e6c973 --- /dev/null +++ b/.github/scripts/pr_checklist_check.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import sys +from typing import List + +# Matches a Markdown checklist item in the PR body. +# Expected format: "- [ ] Task description" or "* [x] Task description" +# Group 1 captures the checkbox state: ' ' (unchecked), 'x' or 'X' (checked). +# Group 2 captures the task content (the description of the checklist item). +TASK_PATTERN = re.compile(r'^\s*[-*]\s+\[( |x|X)\]\s*(.*)') + + +def find_all_tasks(pr_body: str) -> List[str]: + """Return list of all task list items (both resolved and unresolved).""" + tasks: List[str] = [] + for line in pr_body.splitlines(): + match = TASK_PATTERN.match(line) + if match: + tasks.append(match.group(0).strip()) + return tasks + + +def find_unresolved_tasks(pr_body: str) -> List[str]: + """Return list of unresolved task list items. + + A task is considered resolved if it is checked (``[x]`` or ``[X]``) + or if its text is struck through using ``~~`` markers. + """ + unresolved: List[str] = [] + for line in pr_body.splitlines(): + match = TASK_PATTERN.match(line) + if not match: + continue + state, content = match.groups() + if state.lower() == 'x': + continue + # Check if the entire content is struck through + if content.strip().startswith('~~') and content.strip().endswith('~~'): + continue + unresolved.append(match.group(0).strip()) + return unresolved + + +def check_pr_checklist_section(pr_body: str) -> tuple[bool, str]: + """Check if the PR Checklist section exists with the required final checkbox. + + Returns: + tuple: (is_valid, error_message) + """ + # Check if "## PR Checklist" header exists + pr_checklist_pattern = re.compile(r'^##\s+PR\s+Checklist', + re.IGNORECASE | re.MULTILINE) + if not pr_checklist_pattern.search(pr_body): + return False, "Missing '## PR Checklist' header. Please ensure you haven't removed the PR template section." + + # Check if the final checkbox exists (the one users must check) + final_checkbox_pattern = re.compile( + r'^\s*[-*]\s+\[( |x|X)\]\s+Please check this after reviewing the above items', + re.MULTILINE) + if not final_checkbox_pattern.search(pr_body): + return False, "Missing the required final checkbox '- [ ] Please check this after reviewing the above items as appropriate for this PR.' Please ensure you haven't removed this from the PR template." + + return True, "" + + +def main() -> None: + pr_body = os.environ.get("PR_BODY", "") + enforce_checklist = os.environ.get("ENFORCE_PR_HAS_CHECKLIST", + "false").lower() == "true" + + # Always check for PR Checklist section when enforcement is enabled + if enforce_checklist: + is_valid, error_msg = check_pr_checklist_section(pr_body) + if not is_valid: + print(f"Error: {error_msg}") + sys.exit(1) + + all_tasks = find_all_tasks(pr_body) + unresolved = find_unresolved_tasks(pr_body) + + # Check if we need to enforce the presence of at least one checklist item + if enforce_checklist and not all_tasks: + print( + "Error: PR body must contain at least one checklist item when ENFORCE_PR_HAS_CHECKLIST is enabled." + ) + print( + "Expected format: - [ ] Task description or * [ ] Task description") + sys.exit(1) + + # If we have tasks, check if any are unresolved + if unresolved: + print("Unresolved checklist items found:") + for item in unresolved: + print(f"{item}") + sys.exit(1) + + if all_tasks: + print("All checklist items resolved.") + else: + print("No checklist items found in PR body.") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 0668283cc30..787e5ac0132 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -53,3 +53,21 @@ jobs: echo " - [#1234][doc] Update documentation" echo " - [None][chore] Minor clean-up" exit 1 + + check-pr-body-checklist: + name: Check PR Checklist Resolution + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Validate PR Checklist + env: + PR_BODY: ${{ github.event.pull_request.body }} + ENFORCE_PR_HAS_CHECKLIST: true + run: python .github/scripts/pr_checklist_check.py