diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b3c24ef..7864c9b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,5 @@ ## ℹ️ Overview + **REPLACE ME**: Provide the context and description of the change. diff --git a/.github/scripts/validate-pr-description.js b/.github/scripts/validate-pr-description.js new file mode 100644 index 0000000..5c05522 --- /dev/null +++ b/.github/scripts/validate-pr-description.js @@ -0,0 +1,65 @@ +/** + * Validate that a PR description has a filled Overview section. + * Intended for use with actions/github-script. + * + * @param {{ core: import('@actions/core'), context: import('@actions/github').Context }} params + * @param {{ minOverviewLength?: number }} options + */ +/** + * Revert PRs are exempt from description validation. Detection lives here rather than + * in a workflow `if` condition so the job still completes successfully instead of + * showing as skipped, which can block merges when this check is required. + */ +function isRevertPr(pr) { + const title = pr?.title ?? ''; + const headRef = pr?.head?.ref ?? ''; + return /^Revert\s/i.test(title) || /^revert[-_]/i.test(headRef); +} + +module.exports = async function validatePrDescription({ core, context }, options = {}) { + const minOverviewLength = Number(options.minOverviewLength) || 40; + const pr = context.payload.pull_request; + + if (isRevertPr(pr)) { + core.info('Skipping PR description check for revert PR.'); + return; + } + + const body = pr?.body ?? ''; + + if (!body.trim()) { + core.setFailed('PR description is empty. Please add a description with enough context for reviewers.'); + return; + } + + if (/\*\*REPLACE ME\*\*/.test(body)) { + core.setFailed('PR description still contains the "REPLACE ME" placeholder. Please fill in the overview.'); + return; + } + + const withoutComments = body.replace(//g, ''); + const overviewSection = withoutComments + .split(/\n(?=## )/) + .find((section) => /^##[^\n]*overview/i.test(section.trim())); + + if (!overviewSection) { + core.setFailed( + 'PR description must include an "## Overview" section. Use the standard PR template and fill in the overview.' + ); + return; + } + + const overviewText = overviewSection + .replace(/^##[^\n]*\n?/i, '') + .replace(/\s+/g, ' ') + .trim(); + + if (overviewText.length < minOverviewLength) { + core.setFailed( + `Overview section is too short (${overviewText.length} characters, minimum ${minOverviewLength}). Please add a brief description of the change.` + ); + return; + } + + core.info(`PR description check passed (overview: ${overviewText.length} characters).`); +}; diff --git a/.github/workflows/callable.pr-description-check.yaml b/.github/workflows/callable.pr-description-check.yaml new file mode 100644 index 0000000..c443e37 --- /dev/null +++ b/.github/workflows/callable.pr-description-check.yaml @@ -0,0 +1,44 @@ +name: pr-description-check +on: + workflow_call: + inputs: + min_overview_length: + description: Minimum character count required in the Overview section after stripping HTML comments and placeholders. + type: number + required: false + default: 40 + secrets: + GH_TOKEN: + required: true + +jobs: + pr-description-check: + name: PR description check + runs-on: ubuntu-latest + steps: + - name: Checkout workflow scripts + uses: actions/checkout@v7 + with: + # Check out the reusable workflow's own repo/SHA so callers get the + # script version that matches the pinned callable workflow. + repository: ${{ job.workflow_repository }} + ref: ${{ job.workflow_sha }} + sparse-checkout: | + .github/scripts + token: ${{ secrets.GH_TOKEN }} + + - name: Validate PR description + uses: actions/github-script@v9 + env: + MIN_OVERVIEW_LENGTH: ${{ inputs.min_overview_length }} + with: + github-token: ${{ secrets.GH_TOKEN }} + script: | + const script = require('${{ github.workspace }}/.github/scripts/validate-pr-description.js'); + // github, context, and core are injected by github-script from this + // job's runtime context (the caller's workflow run / PR payload). + // Revert PRs (title "Revert ..." or revert-* branch) are exempt — see validate-pr-description.js. + await script( + { github, context, core }, + { minOverviewLength: Number(process.env.MIN_OVERVIEW_LENGTH) } + ); diff --git a/.github/workflows/local.pr-description-check.yaml b/.github/workflows/local.pr-description-check.yaml new file mode 100644 index 0000000..3a49f5f --- /dev/null +++ b/.github/workflows/local.pr-description-check.yaml @@ -0,0 +1,13 @@ +name: pr-description-check +on: + pull_request: + types: + - opened + - edited + - reopened + +jobs: + call-pr-description-check-workflow: + uses: ./.github/workflows/callable.pr-description-check.yaml + secrets: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}