Triage: Check PR requirements #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: 'Triage: Check PR requirements' | |
| # Checks external PRs for minimum requirements: | |
| # 1. PR body must be at least 10 characters | |
| # 2. PR must reference an issue with the 'help-wanted' label | |
| # If requirements are not met, adds 'unmet-requirements' label and comments. | |
| # Also re-checks on PR edits and removes the label if requirements are now met. | |
| on: | |
| workflow_call: | |
| inputs: | |
| min_body_length: | |
| description: 'Minimum PR body length in characters' | |
| type: number | |
| required: false | |
| default: 10 | |
| help_wanted_label: | |
| description: 'Label that must be on the referenced issue' | |
| type: string | |
| required: false | |
| default: 'help-wanted' | |
| unmet_label: | |
| description: 'Label to add when requirements are not met' | |
| type: string | |
| required: false | |
| default: 'unmet-requirements' | |
| triage_label: | |
| description: 'Triage label to manage' | |
| type: string | |
| required: false | |
| default: 'needs-triage' | |
| external_label: | |
| description: 'Label that identifies external PRs' | |
| type: string | |
| required: false | |
| default: 'external' | |
| days_until_close: | |
| description: 'Number of days before closing PRs with unmet requirements' | |
| type: number | |
| required: false | |
| default: 7 | |
| pull_request_target: | |
| types: [labeled, edited] | |
| schedule: | |
| - cron: '10 * * * *' | |
| permissions: | |
| issues: read | |
| pull-requests: write | |
| jobs: | |
| check-requirements: | |
| runs-on: ubuntu-latest | |
| # Run when 'external' label is added, or when a PR with 'unmet-requirements' is edited | |
| # Skip draft PRs | |
| if: | | |
| !github.event.pull_request.draft && | |
| ( | |
| (github.event_name == 'pull_request_target' && github.event.action == 'labeled' && github.event.label.name == 'external') || | |
| (github.event_name == 'pull_request_target' && github.event.action == 'edited' && contains(github.event.pull_request.labels.*.name, 'unmet-requirements')) | |
| ) | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| NUMBER: ${{ github.event.pull_request.number }} | |
| steps: | |
| - name: Check PR requirements | |
| id: check | |
| env: | |
| PRBODY: ${{ github.event.pull_request.body }} | |
| MIN_LENGTH: ${{ inputs.min_body_length || 10 }} | |
| HELP_WANTED_LABEL: ${{ inputs.help_wanted_label || 'help-wanted' }} | |
| run: | | |
| ISSUES_MET="false" | |
| BODY_MET="false" | |
| REASONS="" | |
| # Check body length | |
| BODY_LENGTH=$(echo -n "$PRBODY" | wc -c | tr -d ' ') | |
| if [ "$BODY_LENGTH" -ge "$MIN_LENGTH" ]; then | |
| BODY_MET="true" | |
| else | |
| REASONS="- PR description is missing or too short — please provide a detailed description" | |
| fi | |
| # Collect issue numbers from closingIssuesReferences (structured) and regex fallback (bare mentions) | |
| CLOSING_ISSUES=$(gh pr view "$NUMBER" --json closingIssuesReferences --jq '.closingIssuesReferences[].number' 2>/dev/null || echo "") | |
| REGEX_ISSUES=$(echo "$PRBODY" | grep -oE '#[0-9]+|issues/[0-9]+' | grep -oE '[0-9]+' || echo "") | |
| ISSUE_NUMBERS=$(echo -e "$CLOSING_ISSUES\n$REGEX_ISSUES" | sort -u | grep -v '^$') | |
| if [ -z "$ISSUE_NUMBERS" ]; then | |
| REASONS="$REASONS | |
| - No linked \`$HELP_WANTED_LABEL\` issue found in PR description" | |
| else | |
| # Check each referenced issue for help-wanted label | |
| for ISSUE_NUM in $ISSUE_NUMBERS; do | |
| LABELS=$(gh issue view "$ISSUE_NUM" --json labels --jq '.labels[].name' 2>/dev/null || echo "") | |
| if echo "$LABELS" | grep -qx "$HELP_WANTED_LABEL"; then | |
| ISSUES_MET="true" | |
| break | |
| fi | |
| done | |
| if [ "$ISSUES_MET" = "false" ]; then | |
| REASONS="$REASONS | |
| - None of the referenced issues have the \`$HELP_WANTED_LABEL\` label" | |
| fi | |
| fi | |
| if [ "$BODY_MET" = "true" ] && [ "$ISSUES_MET" = "true" ]; then | |
| echo "requirements_met=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "requirements_met=false" >> $GITHUB_OUTPUT | |
| EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) | |
| echo "reasons<<$EOF" >> $GITHUB_OUTPUT | |
| echo "$REASONS" >> $GITHUB_OUTPUT | |
| echo "$EOF" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Add unmet-requirements label and comment | |
| if: steps.check.outputs.requirements_met == 'false' && github.event.action == 'labeled' | |
| env: | |
| UNMET_LABEL: ${{ inputs.unmet_label || 'unmet-requirements' }} | |
| TRIAGE_LABEL: ${{ inputs.triage_label || 'needs-triage' }} | |
| DAYS: ${{ inputs.days_until_close || 7 }} | |
| REASONS: ${{ steps.check.outputs.reasons }} | |
| run: | | |
| gh pr edit "$NUMBER" --add-label "$UNMET_LABEL" --remove-label "$TRIAGE_LABEL" | |
| gh pr comment "$NUMBER" --body "Thanks for your pull request! Unfortunately, it doesn't meet the minimum requirements for review: | |
| $REASONS | |
| **Please update your PR to address the above.** Requirements: | |
| 1. Include a detailed description of what this PR does | |
| 2. Link to an issue with the \`${{ inputs.help_wanted_label || 'help-wanted' }}\` label (use \`Fixes #123\` or \`Closes #123\` if it resolves the issue) | |
| This PR will be automatically closed in **$DAYS days** if these requirements are not met." | |
| - name: Remove unmet-requirements label if now passing | |
| if: steps.check.outputs.requirements_met == 'true' && github.event.action == 'edited' | |
| env: | |
| UNMET_LABEL: ${{ inputs.unmet_label || 'unmet-requirements' }} | |
| TRIAGE_LABEL: ${{ inputs.triage_label || 'needs-triage' }} | |
| run: | | |
| gh pr edit "$NUMBER" --remove-label "$UNMET_LABEL" --add-label "$TRIAGE_LABEL" | |
| gh pr comment "$NUMBER" --body "Thank you for updating your pull request! It now meets the minimum requirements and has been queued for triage." | |
| close-unmet-requirements: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'schedule' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| steps: | |
| - name: Close PRs with unmet requirements past deadline | |
| env: | |
| UNMET_LABEL: ${{ inputs.unmet_label || 'unmet-requirements' }} | |
| DAYS: ${{ inputs.days_until_close || 7 }} | |
| HELP_WANTED_LABEL: ${{ inputs.help_wanted_label || 'help-wanted' }} | |
| MIN_LENGTH: ${{ inputs.min_body_length || 10 }} | |
| TRIAGE_LABEL: ${{ inputs.triage_label || 'needs-triage' }} | |
| run: | | |
| CUTOFF_DATE=$(date -u -d "$DAYS days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-${DAYS}d +%Y-%m-%dT%H:%M:%SZ) | |
| gh pr list --label "$UNMET_LABEL" --state open --json number,createdAt,body,labels --jq '.[]' | while read -r PR_JSON; do | |
| PR_NUM=$(echo "$PR_JSON" | jq -r '.number') | |
| PRBODY=$(echo "$PR_JSON" | jq -r '.body // ""') | |
| # Find when unmet-requirements label was added | |
| LABEL_ADDED=$(gh api "repos/$GH_REPO/issues/$PR_NUM/events" --jq "[.[] | select(.event == \"labeled\" and .label.name == \"$UNMET_LABEL\")] | last | .created_at") | |
| if [ -z "$LABEL_ADDED" ] || [ "$LABEL_ADDED" = "null" ]; then | |
| continue | |
| fi | |
| if [[ "$LABEL_ADDED" < "$CUTOFF_DATE" ]]; then | |
| # Skip draft PRs | |
| IS_DRAFT=$(gh pr view "$PR_NUM" --json isDraft --jq '.isDraft' 2>/dev/null || echo "false") | |
| if [ "$IS_DRAFT" = "true" ]; then | |
| continue | |
| fi | |
| # Re-check requirements before closing | |
| BODY_LENGTH=$(echo -n "$PRBODY" | wc -c | tr -d ' ') | |
| ISSUES_MET="false" | |
| if [ "$BODY_LENGTH" -ge "$MIN_LENGTH" ]; then | |
| CLOSING_ISSUES=$(gh pr view "$PR_NUM" --json closingIssuesReferences --jq '.closingIssuesReferences[].number' 2>/dev/null || echo "") | |
| REGEX_ISSUES=$(echo "$PRBODY" | grep -oE '#[0-9]+|issues/[0-9]+' | grep -oE '[0-9]+' || echo "") | |
| ISSUE_NUMBERS=$(echo -e "$CLOSING_ISSUES\n$REGEX_ISSUES" | sort -u | grep -v '^$') | |
| for ISSUE_NUM in $ISSUE_NUMBERS; do | |
| LABELS=$(gh issue view "$ISSUE_NUM" --json labels --jq '.labels[].name' 2>/dev/null || echo "") | |
| if echo "$LABELS" | grep -qx "$HELP_WANTED_LABEL"; then | |
| ISSUES_MET="true" | |
| break | |
| fi | |
| done | |
| fi | |
| if [ "$ISSUES_MET" = "true" ] && [ "$BODY_LENGTH" -ge "$MIN_LENGTH" ]; then | |
| # Requirements now met — remove label, restore triage | |
| gh pr edit "$PR_NUM" --remove-label "$UNMET_LABEL" --add-label "$TRIAGE_LABEL" | |
| gh pr comment "$PR_NUM" --body "Thank you for updating your pull request! It now meets the minimum requirements and has been queued for triage." | |
| else | |
| gh pr close "$PR_NUM" | |
| fi | |
| fi | |
| done |