name: Validate PR template on: pull_request_target: types: [opened, edited, synchronize, reopened] workflow_dispatch: permissions: contents: read pull-requests: read jobs: check: runs-on: ubuntu-latest steps: - name: Check PR author permissions id: check if: github.event_name == 'pull_request_target' && github.event.pull_request.draft == false uses: actions/github-script@v9 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const pr = context.payload.pull_request; const {owner, repo} = context.repo; const actor = pr.user.login; const authorType = pr.user.type; // Check if PR author is a bot (e.g., Dependabot) if (authorType === 'Bot') { core.setOutput('skip_validation', 'true'); console.log(`Skipping validation for bot-authored PR: ${actor}`); return; } // Check if this is a community provider PR (only modifies COMMUNITY_PROVIDERS.md) const { data: files } = await github.rest.pulls.listFiles({ owner, repo, pull_number: pr.number }); const isCommunityProviderPR = files.length === 1 && files[0].filename === 'COMMUNITY_PROVIDERS.md'; if (isCommunityProviderPR) { core.setOutput('is_community_provider', 'true'); console.log('Community provider PR detected - relaxed validation will apply'); } else { core.setOutput('is_community_provider', 'false'); } // Get permission level try { const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: actor }); const permission = data.permission; // admin|maintain|write|triage|read|none console.log(`Actor ${actor} has permission level: ${permission}`); // Check if user has write+ permissions if (['admin', 'maintain', 'write'].includes(permission)) { core.setOutput('skip_validation', 'true'); console.log(`Skipping validation for maintainer: ${actor} (${permission})`); } else { core.setOutput('skip_validation', 'false'); console.log(`Validation required for: ${actor} (${permission})`); } } catch (e) { // If we can't determine permissions, require validation core.setOutput('skip_validation', 'false'); core.warning(`Permission lookup failed: ${e.message}`); } - name: Validate PR template if: | github.event_name == 'pull_request_target' && github.event.pull_request.draft == false && steps.check.outputs.skip_validation != 'true' env: PR_BODY: ${{ github.event.pull_request.body }} IS_COMMUNITY_PROVIDER: ${{ steps.check.outputs.is_community_provider }} run: | printf '%s\n' "$PR_BODY" | tr -d '\r' > body.txt # Required sections from the template required=( "# Description" "# How Has This Been Tested?" "# Checklist" ) err=0 # Check for required sections for h in "${required[@]}"; do grep -Fq "$h" body.txt || { echo "::error::$h missing"; err=1; } done # Check for issue reference - relaxed for community provider PRs if [ "$IS_COMMUNITY_PROVIDER" = "true" ]; then # For community provider PRs, accept either "Fixes #" or "Related to #" (case-insensitive) if ! grep -Eiq '(Fixes #[0-9]+|Related to #[0-9]+)' body.txt; then echo "::error::Issue reference missing (need 'Fixes #NNN' or 'Related to #NNN')" err=1 fi else # For other PRs, require "Fixes #" with a number if ! grep -Eq 'Fixes #[0-9]+' body.txt; then echo "::error::Missing 'Fixes #NNN' reference" err=1 fi fi # Check for placeholder text that should be replaced grep -Eiq 'Replace this with|Choose one:' body.txt && { echo "::error::Template placeholders still present"; err=1; } # Also check for the unmodified issue number placeholder grep -Fq 'Fixes #[issue number]' body.txt && { echo "::error::Issue number placeholder not updated"; err=1; } exit $err - name: Log skip reason if: | github.event_name == 'pull_request_target' && (github.event.pull_request.draft == true || steps.check.outputs.skip_validation == 'true') run: | echo "Skipping PR template validation. Draft: ${{ github.event.pull_request.draft }}; skip_validation: ${{ steps.check.outputs.skip_validation || 'N/A' }}"